Compare commits
3292 Commits
v3.7.4-bug
...
v5.3.2-412
| Author | SHA1 | Date | |
|---|---|---|---|
| 1478b59479 | |||
| dd1f399425 | |||
| 3e8923f34a | |||
| 41f7e3a05a | |||
| 25ce3ccdc9 | |||
| 5df0d85004 | |||
| 63b88383a4 | |||
| 36bc35a7e3 | |||
| 1c55ab8f86 | |||
| 9a1f3b0fe3 | |||
| 913bcdeeb2 | |||
| e40857d24b | |||
| d3fab1aa73 | |||
| d32698ca91 | |||
| 7832276560 | |||
| 2fca73dfe8 | |||
| 0f194a80bb | |||
| bb60f7f22a | |||
| 30ae815717 | |||
| 1eb1b6a956 | |||
| f46b303a67 | |||
| a1ecb784e2 | |||
| 1f43b8b220 | |||
| 88098a8255 | |||
| 37460ceac2 | |||
| 6dfded50d6 | |||
| 5e01ecf0a2 | |||
| a1be42a135 | |||
| f4a5dd47fc | |||
| b9554d11a8 | |||
| b910075c4c | |||
| 1966e844bd | |||
| bfa236f802 | |||
| 7f64b51582 | |||
| 4324126660 | |||
| a8c54de47d | |||
| 562b3e9d3b | |||
| 679c6da972 | |||
| 793adb9a03 | |||
| 23c62f1092 | |||
| 516d8d8044 | |||
| ed2b41bc55 | |||
| 5351ffd6b6 | |||
| c9bef1fb1a | |||
| 454e8bb934 | |||
| e587d0daaa | |||
| fcc23ddfc2 | |||
| c541805bed | |||
| 2c38c8fd91 | |||
| 15385cf78c | |||
| 6df3a8ec51 | |||
| bca9c6d96f | |||
| 26feccc28b | |||
| a1fc7d2e48 | |||
| 0b42b10e41 | |||
| 0a0912dbab | |||
| 5e3ff2de31 | |||
| 55db52f8f0 | |||
| 2888ab56cd | |||
| 1edc20f665 | |||
| 72c818b17b | |||
| 7393dc83f5 | |||
| 6043553a39 | |||
| 87461b52ce | |||
| df807d746f | |||
| 9e7a519806 | |||
| 6ee4af82ad | |||
| 75c8a4597c | |||
| de274f0954 | |||
| 82195950b3 | |||
| 806b3ac77e | |||
| ae95bd1db8 | |||
| 62ffefb92f | |||
| 1c739a6269 | |||
| f5386cd3c1 | |||
| 7540659396 | |||
| 056985531b | |||
| 730739077b | |||
| 6c82184d1d | |||
| d3b6f6a1e4 | |||
| d35550900b | |||
| dfc16e38eb | |||
| 08bd5f3081 | |||
| 7d98fde53a | |||
| 486e9ae0c5 | |||
| d51587aa66 | |||
| 1fabcc756d | |||
| ee3fc058e1 | |||
| e66e9e1ba8 | |||
| 1cb4bea73c | |||
| b3a491e723 | |||
| 9709c6cbf0 | |||
| ca81eb2ecb | |||
| 910d886f65 | |||
| 376411c0cf | |||
| ad019eff69 | |||
| 3712e67594 | |||
| c98084ec39 | |||
| 5a851ea99d | |||
| 849cdb234c | |||
| 9f9313b5e7 | |||
| 1888d8cfc7 | |||
| d8f9a7ed7f | |||
| 7be4541f89 | |||
| 2d84309f04 | |||
| f2ed7f8b72 | |||
| c839d535d8 | |||
| 623e155c75 | |||
| b20a74de00 | |||
| c9402dec4e | |||
| 636a826583 | |||
| 4aacc425a9 | |||
| c7db544684 | |||
| c7833bae11 | |||
| 3d777781b8 | |||
| b72f9819d7 | |||
| 5f134fad33 | |||
| 21cec51fad | |||
| 048d3aa361 | |||
| 8871d12fa8 | |||
| 67f931f9c8 | |||
| 2fe9d6c3e1 | |||
| a662d5b90c | |||
| c30ec4bd87 | |||
| 094a8abfe2 | |||
| e1c39e90a9 | |||
| aca3f2d2fc | |||
| b5d8d40462 | |||
| c1b7c380da | |||
| e4f0ba0495 | |||
| ccd4a479a9 | |||
| 92941bcc38 | |||
| 6b55875547 | |||
| a0947a84d9 | |||
| fca0e03180 | |||
| 3dad7d8850 | |||
| 1ebe8eba4e | |||
| 3ec3388b08 | |||
| 71c2e8eb6b | |||
| 009aa9f73e | |||
| 14e38bc738 | |||
| 74ffd80b10 | |||
| cb7a04ff09 | |||
| 96640ad91e | |||
| 91017caac4 | |||
| 5d8720687a | |||
| 75b1234376 | |||
| 1e866a9eab | |||
| 80e58080c4 | |||
| 394036a0d0 | |||
| 2f2355f36e | |||
| e711e30c21 | |||
| e3d32057ee | |||
| e42c4e595e | |||
| 73d71e5dac | |||
| d6cad14e7b | |||
| 9beaf93595 | |||
| 074610a9f0 | |||
| 7881edaf21 | |||
| f2643308f9 | |||
| 1168be9a92 | |||
| c34d6141bc | |||
| e1f62e3a3a | |||
| 1d067dbaa2 | |||
| dca3542cec | |||
| 4a37af4ce4 | |||
| a2e907faeb | |||
| 7e33243fbb | |||
| a4603213b9 | |||
| da9ea61fbf | |||
| 4ceef741ea | |||
| d323fc07f9 | |||
| 2356e175f9 | |||
| 7a9c0ed8f2 | |||
| d406cf3fcb | |||
| 456519e06c | |||
| 638e0c988f | |||
| 40aaabad7d | |||
| 46cf8cfbdc | |||
| 2226bcf515 | |||
| 38dcdaf8e3 | |||
| 2a440e91fa | |||
| 2cecb9a14c | |||
| 241f976866 | |||
| 560c270723 | |||
| 5159957722 | |||
| e45218de17 | |||
| 0ff34bd3f7 | |||
| 4a04f6bb0f | |||
| 633b7df021 | |||
| 389c20b6bf | |||
| 20149b6041 | |||
| b764a43abe | |||
| c3fb8b9c89 | |||
| d574b09dfa | |||
| 7fd7197909 | |||
| 58178d2871 | |||
| 880838c263 | |||
| c196d5cdf5 | |||
| c370edaa72 | |||
| 0640aee554 | |||
| 8a5f11e6bd | |||
| 92b6ebcb08 | |||
| 758c331af5 | |||
| 17e5c0535d | |||
| 66fee84b49 | |||
| bbe0350f2c | |||
| b31fe4b94a | |||
| 11b414fd29 | |||
| 1a35b5ded0 | |||
| 4245c1511e | |||
| 989c3706dd | |||
| 49500768db | |||
| 65becdac85 | |||
| 125c78fccd | |||
| 58c576b950 | |||
| b08aa46992 | |||
| c77f0cf817 | |||
| bb0378c216 | |||
| f2e7a64e99 | |||
| 759cacf933 | |||
| 0b213253ce | |||
| f0ab333892 | |||
| 7516db3a60 | |||
| 29cd798ca8 | |||
| 40cb902baf | |||
| 15bbcf3eae | |||
| f508e103c1 | |||
| 94fa36125d | |||
| 1c754a6dd0 | |||
| 609e4a49b8 | |||
| 98710c9542 | |||
| 0e2f5fdf53 | |||
| e9ed5530d5 | |||
| 113f1d3ed8 | |||
| 3fc60a57b6 | |||
| d14e09187e | |||
| bf90131841 | |||
| 40b7a16b21 | |||
| 882d7273a1 | |||
| 6cfeddf3d4 | |||
| a06e6c581d | |||
| bd8bda40ec | |||
| efd81a4e0c | |||
| 493e08ce2c | |||
| 1906451060 | |||
| f865e95ae1 | |||
| 480e18b79a | |||
| 6436bc6191 | |||
| 7ec7eb29e4 | |||
| 5deb744ba3 | |||
| e908939560 | |||
| e02708bb0e | |||
| 4826c35a70 | |||
| c51f185438 | |||
| 65bd15b573 | |||
| 5283254c1a | |||
| 15458df982 | |||
| 0e27e8ee3b | |||
| 17455efe82 | |||
| e2c80cb416 | |||
| a48b4db550 | |||
| 1d66637b11 | |||
| 3e7e98d555 | |||
| e737d16f0f | |||
| a04219518b | |||
| ce2c6bed0a | |||
| 84a08493e3 | |||
| 3d411fa49c | |||
| c0a0c90a49 | |||
| a67d41296e | |||
| fac255a27c | |||
| 0811c8dc15 | |||
| 7537963a8e | |||
| 36f8b1de0b | |||
| 1d9e1bc9d8 | |||
| bb9fd24068 | |||
| 9c044ae98b | |||
| 9807883d65 | |||
| 04e410615d | |||
| eda1eea2de | |||
| c1f6ef35f2 | |||
| b61ce1e42a | |||
| 62e60f4309 | |||
| c6f544136f | |||
| d588315759 | |||
| b1fef73c54 | |||
| c4fc31c963 | |||
| 85b0d0eef4 | |||
| 812eb842e2 | |||
| 783cb95f24 | |||
| 63b7f294c4 | |||
| 80c95b0f11 | |||
| 5969fa2ca5 | |||
| 0187608918 | |||
| e2fa4990bd | |||
| 6753d04817 | |||
| 98531376d9 | |||
| 86edc8b919 | |||
| 7903751d85 | |||
| 2620a29a2b | |||
| 5cc40c09dc | |||
| 5c02d37852 | |||
| 2e5d445d65 | |||
| c8bae7d89b | |||
| a5aceb3d1f | |||
| 3ef7343309 | |||
| 2c98c38721 | |||
| 4f28c54591 | |||
| 06b5b885e9 | |||
| 612cc2ca9b | |||
| 18f7b695e5 | |||
| f29e2fe1de | |||
| 799c22093f | |||
| 2496c1d96e | |||
| 8ee76af30f | |||
| 2f35442558 | |||
| 81fa6a6233 | |||
| e5778d5b5f | |||
| f69d607ecb | |||
| 79fc2e1f93 | |||
| 6dfdbb9ee8 | |||
| 5d8f7b3f8d | |||
| cd4601a9c9 | |||
| 462a011401 | |||
| 31d55dae14 | |||
| 8dc9731299 | |||
| 66c19c644e | |||
| 2b617e2697 | |||
| 3a60a497e1 | |||
| a9337aee63 | |||
| c80cd47ccc | |||
| d9c2371488 | |||
| 096d19751a | |||
| ef70119090 | |||
| 95dba71bb9 | |||
| ec37c7a6f8 | |||
| dcc9352301 | |||
| c509c6bb38 | |||
| 4784d689f8 | |||
| 8903a075be | |||
| 48d6e91b0e | |||
| 30865239d2 | |||
| 8b7cd92ae8 | |||
| 69f336553e | |||
| ed82f96ae3 | |||
| 8f02016a76 | |||
| c06f397d12 | |||
| 912b7280cd | |||
| 50f7dd2c63 | |||
| 23469543c7 | |||
| 2d3f70dd12 | |||
| f57d19e797 | |||
| 8c529f0724 | |||
| 1583f1957a | |||
| 676ccb133f | |||
| 489b8143a1 | |||
| c4cf3efa21 | |||
| 871cfd638d | |||
| 1f3ae1c687 | |||
| 59eb101a48 | |||
| b3b8b6ba29 | |||
| 42baaa8950 | |||
| 61ad70d3b8 | |||
| 04252e3b91 | |||
| b40543d7b5 | |||
| 903fa49b71 | |||
| 03ae2699c6 | |||
| 8ecc4078bb | |||
| ebaf4f02f0 | |||
| e85437379a | |||
| 2e9642019b | |||
| 5a77522a04 | |||
| e5512604fa | |||
| 146a46f8be | |||
| 91f33f06d3 | |||
| 59da7b6ba6 | |||
| 0a1ad238f8 | |||
| 302e6c145c | |||
| a6bbf0dfc7 | |||
| 9c3d22964c | |||
| ab60ed8473 | |||
| 9a00e70cc4 | |||
| 35954f52c5 | |||
| e13d80d063 | |||
| 73ed15689d | |||
| 65d949bade | |||
| 33d0d01051 | |||
| 9998ed0a14 | |||
| 85a8a17acb | |||
| 4032cb8abe | |||
| fc2e87063e | |||
| a63b809642 | |||
| c30048c268 | |||
| 956d5a39be | |||
| 76fd1ab7e3 | |||
| 2c4c954c64 | |||
| aef4da961d | |||
| 31e00bb681 | |||
| 369fdd24ef | |||
| 2a57994c7f | |||
| 6f84b14277 | |||
| 10ed05c8ef | |||
| 799bd1042f | |||
| 6f9acab2d2 | |||
| 387d8eb5af | |||
| c813c5d5ef | |||
| 1c6df6741f | |||
| b54ab5f824 | |||
| 37fdf6c18e | |||
| 2685de9b3e | |||
| c049100f5f | |||
| 711d1f9d65 | |||
| 5d7453afec | |||
| e4acf466ba | |||
| 2d66cbd29c | |||
| 8786f7d500 | |||
| 3d40d7a819 | |||
| 6f7862a0f2 | |||
| 58c6211d54 | |||
| d10b187a1f | |||
| 1ebf34c83a | |||
| 01e2a3c708 | |||
| c9b97552d4 | |||
| 428f39aa86 | |||
| 5fecb723bc | |||
| 0168b0d93a | |||
| 895fa510fe | |||
| 0aae59a1ba | |||
| 574a2b4dc7 | |||
| acae608966 | |||
| 6ca6c34f92 | |||
| 16724a2d2e | |||
| 8b50cc561a | |||
| 1ec1d482c4 | |||
| 869b9d507b | |||
| 514fe66347 | |||
| 657c91a5ef | |||
| 82354a1156 | |||
| b7619f0c93 | |||
| 598a3ad0b6 | |||
| bf0be93dc9 | |||
| 90a676f778 | |||
| a675e4bbe3 | |||
| 5eee8c6785 | |||
| 3c54e4313c | |||
| 67c4b76de9 | |||
| 34fd08d06a | |||
| d08b95021d | |||
| 8517a2aef1 | |||
| cbe90141f1 | |||
| fd43bc9426 | |||
| 34a143b632 | |||
| d96e02eca0 | |||
| 398b1ae58b | |||
| 0749205bef | |||
| 54ec115fee | |||
| ebe7b84dc8 | |||
| 332abe66f5 | |||
| 9dccbfbd51 | |||
| 23a8c9e6aa | |||
| 1e4408ac6e | |||
| 05ece9c999 | |||
| 216905e455 | |||
| a1bd88dad8 | |||
| 16c66c707f | |||
| 50cdea2b03 | |||
| 95b0fda15a | |||
| 9c99273f81 | |||
| 8d9d36a5df | |||
| b22d168e97 | |||
| 1fee166e3b | |||
| 7c098c29b2 | |||
| ffc9dc46e2 | |||
| c491a4084c | |||
| 0f076a03c5 | |||
| 3345ab2044 | |||
| 5874bdd311 | |||
| 4669211196 | |||
| 4879e7f0aa | |||
| f5283d386b | |||
| 602333f923 | |||
| e9810c129e | |||
| c503f15df9 | |||
| cba1012bbd | |||
| 646493ae61 | |||
| b649089ab0 | |||
| e97fd9023e | |||
| 478dd658ab | |||
| b313feac01 | |||
| c04b82549e | |||
| 842e62f909 | |||
| 51c6de0506 | |||
| d350f65c1f | |||
| aaf8e28abc | |||
| 7cb921e969 | |||
| 3a4b41058d | |||
| 26308d1852 | |||
| af54932309 | |||
| 83799048ed | |||
| ce410ad2a0 | |||
| 933c40458f | |||
| 6ae7578281 | |||
| 72b804e26e | |||
| 7e58cdc59e | |||
| d782ea90b6 | |||
| efffd752f5 | |||
| 47f3f45fe6 | |||
| b4e12e2ec3 | |||
| 2d331f6294 | |||
| feb4137f84 | |||
| 6a2c4a2967 | |||
| 68fc5532ff | |||
| 43ae479271 | |||
| 7a3ea4d939 | |||
| 1bcb343355 | |||
| 1b7a601cee | |||
| 8bc9544dc2 | |||
| e586e65e44 | |||
| 8096da5f50 | |||
| 91868fda15 | |||
| a71e8824ce | |||
| 6cbc354921 | |||
| 79e2077a3d | |||
| 6ea3ed067d | |||
| 84fe33becc | |||
| 276f53fead | |||
| 0789cc6fdf | |||
| 3a563b7bd0 | |||
| 9b8ba0c3e6 | |||
| 258f054161 | |||
| 258c8db0f9 | |||
| 741d0b11af | |||
| 9267896b50 | |||
| 85c0bd69c1 | |||
| 2436507b7b | |||
| 1b68b07e53 | |||
| c4ba28de1a | |||
| 8daa55c16a | |||
| 5e6c6aca13 | |||
| e08ab790ee | |||
| a394901cb4 | |||
| 2aaa7f2795 | |||
| 7b0d2a25c8 | |||
| e3f553a96f | |||
| 809a25e3e2 | |||
| 88041d9b5f | |||
| a11d741f6f | |||
| 1283def6da | |||
| 9c621c31a2 | |||
| 9e3f40723f | |||
| 682bb9a3e7 | |||
| 9f2d164c70 | |||
| bad5130047 | |||
| e3f831e0ca | |||
| a1432452f5 | |||
| b3cc68b7c6 | |||
| cc1f5a269a | |||
| a2892a713a | |||
| c342e22ce4 | |||
| 57dcbab096 | |||
| 5aa4c5bcbc | |||
| 039e1fc957 | |||
| f7b2d10543 | |||
| 38972eca76 | |||
| c2d853d709 | |||
| 239209dbf2 | |||
| ba1f3e20d3 | |||
| 92ac62d949 | |||
| 6499e3b718 | |||
| 5f0d2604ec | |||
| 5f7bb6cb49 | |||
| 7360eef244 | |||
| 52e6feab59 | |||
| 5f44252410 | |||
| 7ae3b1cafb | |||
| 0fb77e79fd | |||
| d4ae878947 | |||
| 37a2e3cc0e | |||
| 30595741cc | |||
| 11f177d572 | |||
| 9e13675ca5 | |||
| 144a0641b6 | |||
| 95266f6f68 | |||
| 9ded647c0d | |||
| f566e4d5c3 | |||
| 5df3efd087 | |||
| 98f4361bf0 | |||
| 000d1e020a | |||
| 93431a7e37 | |||
| d54523cdb6 | |||
| ed468b7c73 | |||
| bad6211bf5 | |||
| 2743d4d0de | |||
| a0f5387917 | |||
| 1abfcfdb51 | |||
| ecc0a75f52 | |||
| c98bf26337 | |||
| 187d07d02d | |||
| c47c23ad21 | |||
| 8d011ae7ef | |||
| 36b58383f7 | |||
| e6ea1738e5 | |||
| 70b7689ac5 | |||
| 40983ecebc | |||
| 47b0a8d1ac | |||
| 1ae63d3010 | |||
| fbe73ce4da | |||
| d46617237f | |||
| 0894957d73 | |||
| 9b9777ae19 | |||
| 7f2145d0ef | |||
| acefbba6bf | |||
| 660a091b7b | |||
| 103078d63e | |||
| 178395f531 | |||
| 8db213d040 | |||
| 5904d3c5c7 | |||
| 1a511ebbad | |||
| ef97e8cc6d | |||
| 814cef6218 | |||
| fd6c4836d4 | |||
| 9df138ed7c | |||
| 7b11b0e99a | |||
| 01034f093a | |||
| 8e28766449 | |||
| d06d0bd15c | |||
| 4fe4a33b80 | |||
| 4a8e91bbff | |||
| b1f6c7d55c | |||
| 727eeac52e | |||
| fb26ec0c13 | |||
| 3e25619bf8 | |||
| e81fa4ff1a | |||
| d7d2bf2667 | |||
| c763a0a9e6 | |||
| 1f34f95e7c | |||
| 2afb99b603 | |||
| 2e09ac033b | |||
| 54747143d1 | |||
| 076c98977d | |||
| b96104fcd9 | |||
| daece9337c | |||
| 3547330e2a | |||
| 4d76fb0159 | |||
| f34a95b36f | |||
| 6e3cb7a196 | |||
| 03f87c46ba | |||
| b052ba1635 | |||
| d1183199cd | |||
| 31fed948f0 | |||
| f1c5b80dc2 | |||
| 86f2aec9ca | |||
| f826dc07c6 | |||
| 34670d08c0 | |||
| 5e5646468e | |||
| a218f13ccb | |||
| 1fc37571df | |||
| 576f06f250 | |||
| 3805cb52f6 | |||
| f8d7377236 | |||
| 354fa13a35 | |||
| 3c533b896b | |||
| 11c6618c0f | |||
| 898bf3e432 | |||
| 0faf408762 | |||
| 1dbabe3cda | |||
| a107a19d95 | |||
| 58ea0bb51d | |||
| 557f22b23b | |||
| f40a192e82 | |||
| 84f3556275 | |||
| cddc2bff2b | |||
| a0b8caa60a | |||
| 22cadc77f0 | |||
| 28fb45f0b8 | |||
| 3d7f8b3f41 | |||
| bba326cfd9 | |||
| f76bb7070a | |||
| a6a3d769bd | |||
| ea5627b3da | |||
| d9807227ec | |||
| 039203408a | |||
| 60b325812e | |||
| 06a43f617b | |||
| 5cc1cfa1a6 | |||
| 35cdd140cc | |||
| 87597dbd1f | |||
| 398f7b6642 | |||
| 85d3412fd8 | |||
| 71b8cbbef3 | |||
| dfc0183a14 | |||
| f0ca0a2ab1 | |||
| c9ffd4e5bd | |||
| d354b07fe2 | |||
| 71a61fdef0 | |||
| 156e52f619 | |||
| 252cb3825b | |||
| 2eded37321 | |||
| 075a7e4e77 | |||
| 0c95f911d1 | |||
| f35baf0e5b | |||
| 49b74c9a37 | |||
| c4240440d1 | |||
| e0205ec060 | |||
| 895024aa09 | |||
| 4d64281c78 | |||
| 95f1692434 | |||
| f1302dd1bb | |||
| fd946703a9 | |||
| 806935a81e | |||
| feb30ff3a4 | |||
| fec34f1efa | |||
| def3b55e51 | |||
| 4549f78ede | |||
| 3054b56f4a | |||
| 4cf5e0f2f4 | |||
| 1a18c82f06 | |||
| b8fbb429f1 | |||
| 005229fe8a | |||
| 3a0bbbf3d0 | |||
| 4b851732c9 | |||
| 3abf29fa75 | |||
| 4bb7d2a043 | |||
| e3d830235e | |||
| 6425f3d50c | |||
| 868e6d31b8 | |||
| a687f8d877 | |||
| e9d2c6573f | |||
| 05e36f11cf | |||
| bc76e6eddb | |||
| 6de543261b | |||
| d9a8fa0a2d | |||
| 51c88e8586 | |||
| 9e930f1b35 | |||
| eb8fb191bb | |||
| 6653467259 | |||
| 9ec29ad367 | |||
| 406ac98616 | |||
| 5832c75909 | |||
| 9d8681c50f | |||
| 9c28bf162e | |||
| 3e86a40215 | |||
| 061d5c19ed | |||
| 1241207dd2 | |||
| 6a68839d0f | |||
| 2140c9eb40 | |||
| 702da08bc4 | |||
| 942af39b2d | |||
| a0331f0437 | |||
| 8c885e38a1 | |||
| acc6935608 | |||
| 63798c94cf | |||
| 62a87d1c71 | |||
| 2ba479d3ca | |||
| 6c2ff6a94f | |||
| 3f5f3bf57c | |||
| 2211f562c7 | |||
| 4d10ca0c6c | |||
| c816fd87f4 | |||
| 547bfd98ac | |||
| 34ba85f099 | |||
| 2d251ef453 | |||
| 7a0165375c | |||
| d7c7fe8740 | |||
| 295e783d4b | |||
| 58dfad4123 | |||
| f31d8b51fa | |||
| 01ea8b9834 | |||
| 5152060515 | |||
| 0a0d805e9d | |||
| 51511c8be8 | |||
| 4bbfa3e16e | |||
| 81c0c6ee09 | |||
| 50b449ca64 | |||
| 8764412492 | |||
| c1e011cae8 | |||
| 018f814a6b | |||
| ef9cb24b61 | |||
| 245a4273de | |||
| 94c1651510 | |||
| 07f55acb3a | |||
| 43ee3b3488 | |||
| 32625f59e0 | |||
| e1e3aa5598 | |||
| c5ff2cb5bd | |||
| acf02c99e7 | |||
| 9a00999d9e | |||
| 4c5b602a3b | |||
| cbde36f3bd | |||
| f4b6193a6b | |||
| 85a596766d | |||
| 785cfecf81 | |||
| 0c8b98084a | |||
| 87a6a3a539 | |||
| 6c50082de5 | |||
| 41195ec9d4 | |||
| e280ea008a | |||
| 223646cf3d | |||
| ab4bb79a32 | |||
| 50b51e2f3c | |||
| 1fef7d51d6 | |||
| 6546961f81 | |||
| 75e4f32840 | |||
| a3f3c95e19 | |||
| 508231d547 | |||
| 8649b98032 | |||
| d35211262f | |||
| 96a5838155 | |||
| fc21799288 | |||
| 3e04d77d50 | |||
| 35f47e363a | |||
| 019724e9c6 | |||
| 70f059b4de | |||
| 34cd63d36d | |||
| 033cf6b566 | |||
| 513fc3be1c | |||
| fd1273a092 | |||
| 27a6f14d18 | |||
| 000c7978fd | |||
| 486c8f691c | |||
| 16450a7f8d | |||
| 438e660449 | |||
| b9bf4311cf | |||
| 75a324747c | |||
| 04b579cda7 | |||
| 5962a8cf08 | |||
| ea388a8e1b | |||
| 8a98d6323a | |||
| 1b67ff5d12 | |||
| 9ee0f65e02 | |||
| 557b6fee21 | |||
| c90d6ec0dc | |||
| 8109aa58c9 | |||
| 213e079e9d | |||
| 40e38e50b9 | |||
| b70e18af96 | |||
| 4c91f06d94 | |||
| 199067cf8a | |||
| fa01260f0b | |||
| f8dac32bd6 | |||
| d5641d437c | |||
| c27f62e726 | |||
| b98d4e6ec6 | |||
| a4e67cda91 | |||
| ab3616f237 | |||
| bd9032c659 | |||
| 4ded8cbfc8 | |||
| 5a14afa71e | |||
| 7736ef37e5 | |||
| 40adf1938e | |||
| 7e1f821d7a | |||
| 4f81996857 | |||
| 328a559ef0 | |||
| ee01512f84 | |||
| a366a87324 | |||
| 3c7a1e2ae3 | |||
| 87c71882f5 | |||
| 7fa912bc0c | |||
| 27dc3f73ae | |||
| be630e39cd | |||
| cded31298c | |||
| 54030fd1df | |||
| 98c1565dfb | |||
| d89c2d4a70 | |||
| 0f11f6344f | |||
| a25f265bd3 | |||
| 68adc0ff23 | |||
| ab5aa01ec5 | |||
| 71e5f798d9 | |||
| 2572c53305 | |||
| 64b5ca8efa | |||
| b5ef73517f | |||
| feb2f68778 | |||
| f0a6e79418 | |||
| 78699cc0e8 | |||
| 96805537f4 | |||
| 13c4be2ab0 | |||
| c2e5e8adbc | |||
| c5c9a3f4d2 | |||
| 8bd2fa9b3a | |||
| 4bd8fc4e51 | |||
| 5b26165aaa | |||
| 1bb4c1cc2c | |||
| d38b347eb8 | |||
| feaf2d0bfc | |||
| 210dcdc698 | |||
| fec291156a | |||
| 62bb23abc9 | |||
| 19be9e1842 | |||
| a5cfd147b4 | |||
| f15d572785 | |||
| 5e99e8b032 | |||
| af39b82da1 | |||
| ba3a4ede11 | |||
| db591d0249 | |||
| 1370261b87 | |||
| 3824de4719 | |||
| 6ee26fb401 | |||
| 865fe2729f | |||
| 85c556c593 | |||
| 86681b8ab1 | |||
| 5f2ce22506 | |||
| dafa85a791 | |||
| 9cc3a29b5a | |||
| d2e8a67971 | |||
| 1bd3168add | |||
| 81d81882e0 | |||
| 3f4e4f1a52 | |||
| 3845e1f3ee | |||
| 6d2ae4dca5 | |||
| e2efa26ac0 | |||
| 70e12ebd08 | |||
| 91fc511fe6 | |||
| c5530ba307 | |||
| 67cbd9b641 | |||
| 6735047e67 | |||
| 9993bca582 | |||
| 07258b08af | |||
| f8bd7787f6 | |||
| b6a3ef5faa | |||
| 730151f6ba | |||
| 532b98e002 | |||
| 202a26b09c | |||
| fe1821884a | |||
| eb1605cc0a | |||
| 08ad5c3455 | |||
| 0774525de6 | |||
| b69ebc8122 | |||
| 49f31da303 | |||
| 66568b09d5 | |||
| 57a944f954 | |||
| 1fb8a60b2b | |||
| 489a57a379 | |||
| 0aa02c186c | |||
| fd463ec1e3 | |||
| 0c16ae5f3f | |||
| 76cca46015 | |||
| 748cacce3f | |||
| b47985d4ff | |||
| 9e41bd2846 | |||
| 0e4b18fc5d | |||
| 5594a03cee | |||
| 59ece3be49 | |||
| a7f77076a5 | |||
| 6526d5b3ce | |||
| 3a2f877d0c | |||
| 0d344e66a1 | |||
| d3de323c06 | |||
| 721d5acc1d | |||
| 7069744191 | |||
| b1f56d5dcd | |||
| 5aaa5bd076 | |||
| 7e74dee0eb | |||
| 45b8f7f82f | |||
| 7c9b8732e2 | |||
| 86ae96253b | |||
| 9dcc0b5587 | |||
| 9ceba793e8 | |||
| 175cc55b75 | |||
| 5486d1803f | |||
| cb885f8b66 | |||
| ab9f5cf7d8 | |||
| 7f4fafc637 | |||
| acccd2330d | |||
| 2745147e73 | |||
| 8fb0e3feaf | |||
| 335fa8ba39 | |||
| c739074057 | |||
| a2569cf876 | |||
| a81b0a1603 | |||
| 76c20e4b82 | |||
| 4144cc2c4f | |||
| 5465ff7731 | |||
| fc6dedb320 | |||
| a76b98f20c | |||
| f05c0324fa | |||
| 606eb30d9f | |||
| aa41da022d | |||
| b62d2c1b9e | |||
| ba21543800 | |||
| 6f5b832a68 | |||
| eb4bbb9efa | |||
| 233f91d2b9 | |||
| 1c51b2fe41 | |||
| 5123d7ff11 | |||
| c20e06f2eb | |||
| df55caa1f8 | |||
| 61bdac3aa2 | |||
| d56dc53b06 | |||
| 18c02ad9c3 | |||
| 8a262532e1 | |||
| 876d920251 | |||
| 1bad70a83f | |||
| c04a1a5802 | |||
| ea8c47a6d6 | |||
| 53f3f48c4d | |||
| 706df0c22f | |||
| 74cee421f9 | |||
| 4b08fa56da | |||
| 522f664f30 | |||
| 87a92055a7 | |||
| f23aa496ae | |||
| 65d7322546 | |||
| d9178f8538 | |||
| 5de471d89a | |||
| 63cc104e9f | |||
| f06ba9bf45 | |||
| 682d5a2cd4 | |||
| 76ce4e2f04 | |||
| 54221cc976 | |||
| 79ae36c51f | |||
| 335d6787a7 | |||
| 2bf9c1e4d7 | |||
| 133635c495 | |||
| 2aaf6d8a9c | |||
| f119f6751a | |||
| 18f964c167 | |||
| 369275225e | |||
| d80daf104f | |||
| 71c697d596 | |||
| 169814d6a9 | |||
| 52cffde739 | |||
| 11f84b69ac | |||
| 2c3e03bd0f | |||
| 264e103395 | |||
| 7f4d3ffe3e | |||
| 79b7a15f7d | |||
| 638dd4ad1d | |||
| 04f58e036b | |||
| 6624868bfe | |||
| 2ff83b8cc5 | |||
| cab412ab70 | |||
| 1a83e64f3a | |||
| be76fb0526 | |||
| 5cc25482a9 | |||
| c683e6786c | |||
| 5f182eeddb | |||
| 3e0979fabb | |||
| 13685ec605 | |||
| 4eac5ff61c | |||
| 98f0cf5fc0 | |||
| cb345f49eb | |||
| ef95be60f1 | |||
| a85cfa47e2 | |||
| 66123b0ae8 | |||
| cdb660b884 | |||
| 026e4f71de | |||
| 01ecde0425 | |||
| f748fcbd48 | |||
| 65464eed3d | |||
| 6a5d8e915c | |||
| 5fde455bcd | |||
| e1e257b1d8 | |||
| 7a87f8dc14 | |||
| 3fe2b8ffcd | |||
| 565b7b2c38 | |||
| b3bf9d862f | |||
| c6f5ef64d5 | |||
| 95b21b5bb5 | |||
| 0da331d109 | |||
| 8c1f8833b1 | |||
| e53cd6dede | |||
| fa27e172e4 | |||
| 544f5102ea | |||
| 5d6134f0dd | |||
| e0b951e043 | |||
| f2476c84c4 | |||
| c186c32d47 | |||
| 194f30650e | |||
| a475b176b4 | |||
| 73cac4d39d | |||
| 93d0f30352 | |||
| 98d30d1183 | |||
| b1d178f8df | |||
| 0550df7039 | |||
| 13c7b8ff1d | |||
| d7b7633ffe | |||
| 5e536db94e | |||
| 55d8b6a866 | |||
| a2a8610083 | |||
| 8d9f0adebe | |||
| e386994e4d | |||
| fc5f1e9830 | |||
| fd17eaea00 | |||
| 8a65d6dd85 | |||
| 8d3c609ad0 | |||
| 2e71e11a09 | |||
| b9c17bcdb8 | |||
| 55bb1a1873 | |||
| 54601bc083 | |||
| df2bb9c020 | |||
| 0bf297d094 | |||
| db8a2a06aa | |||
| 44a9db7294 | |||
| 7db48b6923 | |||
| 0a8f2d51db | |||
| b06d2a479a | |||
| d56bd0078e | |||
| 7b87aadd86 | |||
| 8f2da187f2 | |||
| 8291caa5e3 | |||
| e30ec540bd | |||
| 3c4da21bf6 | |||
| 800260b6a4 | |||
| ddea202f85 | |||
| b3a786ee22 | |||
| 332e24b874 | |||
| 5413ebfba4 | |||
| 7ead1c89f3 | |||
| 03300a41f7 | |||
| 2d8698e070 | |||
| 078b1a18a0 | |||
| de1e3ba7a7 | |||
| a9275c83d2 | |||
| 02894879e3 | |||
| 56a202ae5c | |||
| 013969a73a | |||
| 1a5a4ba149 | |||
| 9bdae33655 | |||
| be1184583b | |||
| 68ee926897 | |||
| 4d0db8cbcc | |||
| 59e4b6e063 | |||
| e74cf92033 | |||
| 811b1bbfce | |||
| 00c25d0d47 | |||
| 66b4b4b9f0 | |||
| 4bab47d2a6 | |||
| 2c9567aa61 | |||
| 97598dc7ec | |||
| 4771c79fa9 | |||
| 3458a5d7f3 | |||
| 5bc7c333d8 | |||
| c7a24555a2 | |||
| e19a678699 | |||
| 795af0528f | |||
| d0b4ea8ec2 | |||
| 45861dff61 | |||
| 5f41463fc3 | |||
| 89cca6dbfd | |||
| e43c0efdcb | |||
| 2dbd635b8e | |||
| 728d4fd8e1 | |||
| 48b5c7efc1 | |||
| 278dcc33be | |||
| 87be3bed61 | |||
| 078b102152 | |||
| 4f6969a70a | |||
| 29140b6c0b | |||
| 11bd9bed95 | |||
| 997b5676dc | |||
| 9b8755e035 | |||
| 85e97bc64d | |||
| 0ea7c81ce9 | |||
| 951ac06948 | |||
| 92cdb6fdbb | |||
| cb0dd1d082 | |||
| 276655a8ec | |||
| df8b612188 | |||
| a550305947 | |||
| 3af4dd5494 | |||
| 4d413eb1f2 | |||
| d496ab0283 | |||
| b519ff6aac | |||
| 8c05d2be8a | |||
| 23dd89198b | |||
| 6569ab2d33 | |||
| 1bead6a756 | |||
| c0df3af298 | |||
| 267363f46d | |||
| 44cf54aee6 | |||
| 322d7117f8 | |||
| c8684f837a | |||
| 9e5ecf1520 | |||
| 2df94dddc5 | |||
| 1731980e88 | |||
| f789f0cb73 | |||
| d043a89f3e | |||
| 4ab0f066df | |||
| 096679029b | |||
| 2634228d21 | |||
| a33784221d | |||
| 2ef7216bda | |||
| 2554fab389 | |||
| a4e3121489 | |||
| 8dfde8ddd0 | |||
| 63403c7c29 | |||
| aa9269aecc | |||
| 489e7105bb | |||
| 8dbc407503 | |||
| 3e032fa540 | |||
| 3df381b255 | |||
| 7c59181bc6 | |||
| 9244a65371 | |||
| a7b8b678f7 | |||
| 9877d4d51e | |||
| 32b4570738 | |||
| af3fdb3cfb | |||
| 5ce992ee63 | |||
| ba17019428 | |||
| f77087907f | |||
| 3080f59ef6 | |||
| 74b1f388e2 | |||
| dcbc2fe352 | |||
| b892c58c52 | |||
| 0a455c7b09 | |||
| 7a254c4609 | |||
| 4f08a4b9d2 | |||
| 75670b7801 | |||
| af386f6bdc | |||
| ff813d8784 | |||
| dba87fe09c | |||
| cf13003c2a | |||
| 7fe337c557 | |||
| 1902ed29c1 | |||
| a67927e9e7 | |||
| 33b6835ae6 | |||
| 1ca6a2c795 | |||
| 372230092a | |||
| 2ee1db1074 | |||
| caabf416bb | |||
| d33d0e98c9 | |||
| a4a029f31e | |||
| 3b8d34f8c3 | |||
| 2368e268bf | |||
| 6390198899 | |||
| 4a6aef4caa | |||
| 91529f51a3 | |||
| 57096b2b08 | |||
| 40f1f57844 | |||
| b9da7fbcb8 | |||
| 7138177df2 | |||
| aa4cf291dc | |||
| 8f4c1651a3 | |||
| 30a6cfd7d9 | |||
| c1abb977bf | |||
| fef633e9d8 | |||
| 5207b5675c | |||
| b7059abcee | |||
| 45a84fc45c | |||
| cf96b00036 | |||
| 49dacc472d | |||
| 72d069ad7e | |||
| 2f49e05891 | |||
| fd2d277470 | |||
| fc52694fdc | |||
| 7123b3d59a | |||
| 7684c247bf | |||
| f2b9d26e8a | |||
| 6d5d407aa8 | |||
| 338de605ec | |||
| 53930b0257 | |||
| 34d5e2cc29 | |||
| 591a694bdf | |||
| 9fd9a4ebcc | |||
| f2712c3db7 | |||
| 61c2c7b218 | |||
| 8a54c47d9e | |||
| 7f60a575a4 | |||
| eb2dd7c6b8 | |||
| ae8433d8bd | |||
| 7ec4ba6582 | |||
| 190434a855 | |||
| 4b82e40eb4 | |||
| d8e858ddb8 | |||
| 9b8c8f328d | |||
| bebdbe64cd | |||
| a0555d6ffc | |||
| d6674efe4a | |||
| 20235c6908 | |||
| bc155d580c | |||
| 9cf852b490 | |||
| f32bc837b4 | |||
| 36efcdb75a | |||
| a232ef9d03 | |||
| 05b90bd5b4 | |||
| bc382e6f31 | |||
| 116d71bb9c | |||
| 2aadb78301 | |||
| 22801846e4 | |||
| f9f6886250 | |||
| 30c8d4db02 | |||
| 08a5ea100e | |||
| db33ed0d0d | |||
| fd424b0204 | |||
| c1bf0a628f | |||
| 086bf75314 | |||
| af31fdc562 | |||
| 99e0a2e0a2 | |||
| 069d0db00e | |||
| 68f1343c93 | |||
| fbe25c9099 | |||
| 9b893bcb42 | |||
| 5182d4cd8d | |||
| cce077de96 | |||
| 03092a0ec1 | |||
| 4906ecc06b | |||
| 7a831ce646 | |||
| 290a26c4e2 | |||
| 571080b1e5 | |||
| d9567138ee | |||
| 4a31f74dcb | |||
| 79cf96b161 | |||
| 7fdd4dbf04 | |||
| 8eeeb5de60 | |||
| 150109cef4 | |||
| 7e7cc38d6d | |||
| b3df30680e | |||
| f73048d3a5 | |||
| c540c14e1c | |||
| f216bf4097 | |||
| a42bcbd160 | |||
| bc6786b21e | |||
| 47e357143c | |||
| 74eb8ad0e8 | |||
| d2fc927ffb | |||
| db4b049591 | |||
| 86db2b5688 | |||
| baa1c9f9e5 | |||
| be3a3e82af | |||
| 120ea26455 | |||
| fc476ca889 | |||
| fc36ce6a03 | |||
| 0a1d330c2f | |||
| e5ec42576c | |||
| b57f74a411 | |||
| cb3884ea2e | |||
| 0cb3184e35 | |||
| 76c5009fbc | |||
| 312ec153f1 | |||
| 1ef79df4c5 | |||
| d7a809f8a6 | |||
| ebadeeb873 | |||
| 55155ee8ae | |||
| be98a6b6d6 | |||
| e2f396f8c7 | |||
| 2a8024368a | |||
| 0cd025b42d | |||
| d6adda78c4 | |||
| 99708d7801 | |||
| 584986a9d5 | |||
| 507da05841 | |||
| 72a8fe4764 | |||
| f9f23f6324 | |||
| e2964f93c7 | |||
| cb2a9a3ca0 | |||
| ea68941bb9 | |||
| b598d331d4 | |||
| d7b876bfed | |||
| 6aca60080a | |||
| 59d76688b9 | |||
| 8e5e677228 | |||
| dd1bfcaddd | |||
| f4c66e1ab7 | |||
| 6ebdf6e42a | |||
| 4c01dd442b | |||
| ac226e3301 | |||
| 569f1b8cf1 | |||
| 46f5da88a6 | |||
| f5b876b018 | |||
| c5df856023 | |||
| 4f775847dc | |||
| f53710fdf5 | |||
| 80c56def08 | |||
| a1d88d999e | |||
| fbc6f6adaa | |||
| e7c55b2467 | |||
| 2c45bb1da9 | |||
| 173825dc74 | |||
| a63e28809b | |||
| 0eb8cb6e66 | |||
| e1514e2b25 | |||
| bef0da821b | |||
| 54741729f3 | |||
| e2640c22f8 | |||
| 2f16d5ba99 | |||
| 1d5301f887 | |||
| cc09c702f7 | |||
| 6b2a88766e | |||
| 0eb629fe67 | |||
| 44c398c9fb | |||
| 96122c5919 | |||
| bd4c438036 | |||
| adc8648f5a | |||
| 8eb3922a33 | |||
| 30743bb015 | |||
| 853f51ad0e | |||
| 763aad99ee | |||
| 2ed2317b02 | |||
| 0663f3eb2b | |||
| 11a2eeae7a | |||
| 8c2619bb22 | |||
| 46320fe07d | |||
| 1ee933b115 | |||
| 1d9152ca1a | |||
| 81ebf21bb9 | |||
| dd4bf98f28 | |||
| ba3a06da43 | |||
| 6fe096383c | |||
| d16f99958f | |||
| 5fa3aa42dc | |||
| f4f28a2daa | |||
| 3c1cd14bdc | |||
| 6dcc251312 | |||
| f692aa4bff | |||
| 7cde55ebe0 | |||
| 94eba08af4 | |||
| 2bf207661c | |||
| 2fff7f60c2 | |||
| 1b3c93ebb5 | |||
| 5b529b044e | |||
| 91f417f479 | |||
| fc0bf595bd | |||
| c0f6499577 | |||
| b9bef384f1 | |||
| 58bfae2fdb | |||
| d2c2fff884 | |||
| ab178c47b5 | |||
| 40be004376 | |||
| 0b501c9ce1 | |||
| cff82379f5 | |||
| 825503bd58 | |||
| 77a715a4bd | |||
| 78b233c506 | |||
| a6e799bb9a | |||
| b65992099a | |||
| 3d1a5f52bf | |||
| 82d0d5745c | |||
| fe7968cb32 | |||
| 94c503af74 | |||
| bdd9815ffc | |||
| 693acceca8 | |||
| 90971bc299 | |||
| e5b3613348 | |||
| 652ca8f69c | |||
| f15e351c1e | |||
| 47c81c3dac | |||
| 7453a61e4f | |||
| 911f6397e8 | |||
| 8d21a4f774 | |||
| 991c54b680 | |||
| 48dcb5089b | |||
| 461efe7101 | |||
| 0735161a20 | |||
| 093c1e2b15 | |||
| 7d0e02c899 | |||
| 8294913f04 | |||
| e811c4f90b | |||
| 69e248b389 | |||
| 43f6bfc4c2 | |||
| c8b81ab56a | |||
| aa41fd98e8 | |||
| ff6c4c2de9 | |||
| 136761d2f7 | |||
| 9374fc5264 | |||
| 324aaa5056 | |||
| f22afbd819 | |||
| e149231cb2 | |||
| 9c8155ddf8 | |||
| dd95419a36 | |||
| 870b10dd13 | |||
| 637b426649 | |||
| ee8ce87e28 | |||
| a944a7f730 | |||
| fdbb16b45f | |||
| 29bc098dcf | |||
| b2433cf13a | |||
| e908e23bb2 | |||
| 1a4dc827b5 | |||
| 9fd5e65fa2 | |||
| 5405dcd30e | |||
| 5c35f7fe5d | |||
| fc907a398f | |||
| 1b8dc6eba0 | |||
| c17a36c866 | |||
| a29cf832f1 | |||
| c9a628a5e9 | |||
| 165ba01afd | |||
| a39a8c2cce | |||
| 4002f138bb | |||
| 48fdb38902 | |||
| 38a3120ea1 | |||
| c0e370dfd2 | |||
| e4765089fa | |||
| 721e73ca1e | |||
| e406c90027 | |||
| e7c4886219 | |||
| 80a2e4f336 | |||
| 9f5940c6f6 | |||
| 0d9a4baf32 | |||
| 3a9132ff8c | |||
| d01fda44b3 | |||
| 02a6ec9f7d | |||
| c779d775e3 | |||
| 3fbcd33f98 | |||
| dbcdd7f3cc | |||
| 69d9854b44 | |||
| 2020033bc0 | |||
| dc2c8e590c | |||
| b115db51e9 | |||
| 5246d6e743 | |||
| b20d598751 | |||
| 85e9799f20 | |||
| 6b533c8d09 | |||
| 2797135db4 | |||
| 7507a027da | |||
| 7d194c7078 | |||
| 9873ae8946 | |||
| a5ef80ba33 | |||
| dfdd12bf18 | |||
| 9daf01d9a5 | |||
| ee7ed1fed2 | |||
| 08474a660e | |||
| d8dfe00057 | |||
| 6bd175f72b | |||
| 72b6b3042a | |||
| 3317b178ac | |||
| c86e7ba6ee | |||
| 14c5e4e963 | |||
| 6db8179f4d | |||
| 65c2571329 | |||
| 4810803988 | |||
| f3f030edb1 | |||
| 2ac2a3bf48 | |||
| cd104e4688 | |||
| e2de8cfb47 | |||
| c9f9451dee | |||
| 73c39edd3f | |||
| 7f739798ab | |||
| e41cf76d26 | |||
| 113d446e47 | |||
| 882ac7d67d | |||
| d94be8092b | |||
| b9138ae810 | |||
| 715cecd44b | |||
| dd756fce00 | |||
| 56a003704d | |||
| 50421e0b70 | |||
| 9f0d00f793 | |||
| a56fd8ff18 | |||
| f86fc7b0e1 | |||
| e6197776ca | |||
| 0415ae261b | |||
| d5313c7f98 | |||
| 360231e01f | |||
| 4d86d6f96b | |||
| dd03daae84 | |||
| 5d285fd60e | |||
| 7d4206bcd8 | |||
| a816636d9a | |||
| c6a5522b35 | |||
| 8b888f0ab4 | |||
| ef41a3aea5 | |||
| ba2206931f | |||
| ef4b7b28d8 | |||
| 4a5f12d341 | |||
| ced96f5f70 | |||
| 4180d87b36 | |||
| 2b7f802584 | |||
| 3e9a348019 | |||
| 9725402623 | |||
| 9827b04057 | |||
| b3052485f5 | |||
| d3dc09d377 | |||
| 58905c14a8 | |||
| aadbbbf2ea | |||
| 5256a9b267 | |||
| 0ded7b33f2 | |||
| cdc3158f30 | |||
| 06de179c9e | |||
| f32dc628ef | |||
| 62e371c928 | |||
| 84bcc481af | |||
| 085ddd4ea6 | |||
| b3e859678e | |||
| 7f586fbf13 | |||
| 9dab3dd263 | |||
| 3ee30f05f0 | |||
| 69c8c51000 | |||
| 28742f54bb | |||
| f62751423d | |||
| b0b699679e | |||
| 3c55284d86 | |||
| b6ae508cf0 | |||
| c13ee31ece | |||
| c87a1ba56e | |||
| 9f07cc7720 | |||
| 7bdfe7cef3 | |||
| 667126f92c | |||
| 0409d33d5e | |||
| 14e8285c0f | |||
| c7d63f9df1 | |||
| e2d9ad07d1 | |||
| 9287b6ac4c | |||
| 450da5e0db | |||
| 0397092414 | |||
| ddc515b490 | |||
| a6edde7853 | |||
| 80282f7bcc | |||
| 664df5cb44 | |||
| 8396a55ed2 | |||
| 5ee3f597bd | |||
| 9c8bd9f85e | |||
| 4321e0a33c | |||
| 6f75229209 | |||
| d6bd561e58 | |||
| c957d7585c | |||
| db0e92fd68 | |||
| 992446cf70 | |||
| dcfa52b05a | |||
| 2a06c41dca | |||
| 24214c5d6b | |||
| c839eff88d | |||
| 663505fdc9 | |||
| 641a820ea5 | |||
| bb3f888f88 | |||
| 75bdf6f251 | |||
| 0cacc15e7d | |||
| e4c2ac0aae | |||
| 7acf4c5941 | |||
| 8b22361213 | |||
| a1dee46436 | |||
| 49bf8c9e30 | |||
| 407686ca5b | |||
| 89b7b45ba0 | |||
| b00e7a2826 | |||
| 383d6e7700 | |||
| edce98e4c6 | |||
| a2c3873c8d | |||
| 7990353d08 | |||
| a7b138b2b2 | |||
| 951768e070 | |||
| 69f29fe83b | |||
| 88e28b2388 | |||
| 538bc6f97c | |||
| b1940125f4 | |||
| e1f8e293c2 | |||
| 0a49f27ed2 | |||
| 5dc7badc97 | |||
| d185d39985 | |||
| d9c98d39b5 | |||
| 6949f2b2ff | |||
| 2141440ec4 | |||
| 464c212638 | |||
| dbe74b2091 | |||
| ba55a5a61c | |||
| 452a94f4a2 | |||
| 310be97fe4 | |||
| 2fc45777e4 | |||
| da283fc1f6 | |||
| fee4e4635c | |||
| ebcd50cdee | |||
| cd922dd286 | |||
| 97598d7330 | |||
| 0db59a56d0 | |||
| bee3f5957e | |||
| 65d4b43c9c | |||
| 41f510d333 | |||
| 64d5af036c | |||
| b4c827bddf | |||
| caf3dfa9ee | |||
| c5c10ab208 | |||
| c2251e1ad6 | |||
| bd6d51dab3 | |||
| 1cf9bfcca3 | |||
| 992ec7bfe8 | |||
| cc39bfd06c | |||
| 15d0ad6f49 | |||
| a2e2379073 | |||
| d2431ed8ff | |||
| 80818dee55 | |||
| f2d0916a16 | |||
| 2fa84e0ce1 | |||
| 6fcd8397b6 | |||
| 3478aaba2f | |||
| 1f0b9a95e2 | |||
| 5cdc1635ca | |||
| c9f5cfd4aa | |||
| d6c1f692be | |||
| 12e6fb8330 | |||
| ad857d4500 | |||
| 43d8d24c1c | |||
| a34ee5a753 | |||
| ca39f723e1 | |||
| 3db784509b | |||
| e8f63ea99f | |||
| 97ac4b03a3 | |||
| 883a948b7b | |||
| ebb279c42f | |||
| 261e0cc45a | |||
| 0357a0a71e | |||
| d87de59427 | |||
| d5ee01a2e1 | |||
| 3563637919 | |||
| d881e08aee | |||
| f5ff838d2f | |||
| 52c1b08e59 | |||
| 652972f49d | |||
| 0d122cf8ba | |||
| 2b115c4058 | |||
| cfca7db4ef | |||
| 5f66c029fb | |||
| 8635652fe2 | |||
| a890ad532c | |||
| 65c9d2be2a | |||
| 9620e26f6a | |||
| e4ca1d8406 | |||
| 74ebaa592b | |||
| f9b689efdb | |||
| ed458f08ad | |||
| 9822646acc | |||
| 4259078420 | |||
| 3b9db4e964 | |||
| b7e6cd08fd | |||
| bba1e6f10c | |||
| 3fe2973968 | |||
| b16d33d9fa | |||
| e1a7ed9049 | |||
| 11354030e8 | |||
| ee109060fc | |||
| eebc0c1096 | |||
| 4ec45e2a27 | |||
| 12143f7f95 | |||
| 7025dc45bd | |||
| 772e7861e2 | |||
| ff71c9a3e8 | |||
| b7001cc996 | |||
| 83658e47f8 | |||
| 0699d3ccc4 | |||
| f80b998e98 | |||
| 91e84be708 | |||
| f589c286c6 | |||
| 379650a0f5 | |||
| 3796eb46bd | |||
| c240048c5e | |||
| 2f81c9240d | |||
| da0dc3df97 | |||
| d73c0f2044 | |||
| a6f9e12082 | |||
| 20f8ba0de5 | |||
| f7342a944e | |||
| 1086574b33 | |||
| 59e30d432d | |||
| 58631d540c | |||
| 08763aeb2d | |||
| 039d0ca06c | |||
| f39574c8b3 | |||
| 6067a52fb1 | |||
| a1298547d1 | |||
| 49d5514a25 | |||
| 09c414a02c | |||
| 6a6378f635 | |||
| 920050efc8 | |||
| 103ecc10d3 | |||
| f90e9a8085 | |||
| caa37ead4b | |||
| d18eeaf346 | |||
| b98d287843 | |||
| 66fb57f708 | |||
| 3911c10827 | |||
| c29972df50 | |||
| 8001ce2603 | |||
| b1079e84d8 | |||
| c6cf8aab1c | |||
| 4d7718e3af | |||
| 9cdbcd4935 | |||
| d396ebda44 | |||
| 6ab8bdc422 | |||
| 15352bb379 | |||
| b68536b561 | |||
| 762ee8d300 | |||
| 8dfda56586 | |||
| 93126517b6 | |||
| 04a8f5772c | |||
| 9687ddcd54 | |||
| 04c49cbad7 | |||
| 6addea7d73 | |||
| 229eaa0859 | |||
| 3876096f52 | |||
| bad3ab19c6 | |||
| b2100fe0e5 | |||
| 5998bb9d4a | |||
| fefe29a0b8 | |||
| a16554da21 | |||
| 6efc1e06bb | |||
| e51439d584 | |||
| 907bad48bc | |||
| 582df7da58 | |||
| 28f18a558c | |||
| 658fb8579c | |||
| 45632052e5 | |||
| 311211747a | |||
| 010995b16d | |||
| c82e981f4e | |||
| f9803fa244 | |||
| f21d86c8a7 | |||
| 8db525542e | |||
| 49cf093a44 | |||
| f2302ffc50 | |||
| f85685fd52 | |||
| a80419e4bd | |||
| 0e7b6bb097 | |||
| b795df2de8 | |||
| e99e8328ab | |||
| 500c77d6ed | |||
| 3c13431bd3 | |||
| d5e426a7f0 | |||
| 8a37e0bdf4 | |||
| 230eec78c1 | |||
| 8bb74461e6 | |||
| b0d0d3ca45 | |||
| 356c9432ec | |||
| 31e14fd47b | |||
| c0644dc705 | |||
| 1a43589bb1 | |||
| defd6d9a14 | |||
| 77b4e8f73d | |||
| 61ef40f9b5 | |||
| 13dd5c9892 | |||
| d20aef7107 | |||
| 8c9cd6e0ec | |||
| 487c2fd0a5 | |||
| 0ffdef080b | |||
| ba76dbbd15 | |||
| 7cfe82af47 | |||
| 567977089a | |||
| a258fe1d20 | |||
| 28cf1ab114 | |||
| 0e49c074ec | |||
| 85249f1d94 | |||
| 38bda5ec02 | |||
| 0cc24956e0 | |||
| c17bcd9765 | |||
| 9f1642f577 | |||
| 9c01f75e8b | |||
| a460722ae5 | |||
| 37330c2ab2 | |||
| 5db25f966e | |||
| 4749c743e9 | |||
| f5b614cc63 | |||
| 34b8b00cb2 | |||
| 6af86d871c | |||
| 0b7fade38b | |||
| 3237834f9a | |||
| 53e9554f08 | |||
| 70ee3001c2 | |||
| d7ae2b2aa4 | |||
| 9c9dbfb105 | |||
| c633c228db | |||
| ad82005881 | |||
| 815a0d0600 | |||
| e02f9db81d | |||
| c422b96da4 | |||
| 40a64996d7 | |||
| cbfca66d51 | |||
| f4a110c94d | |||
| f601fd2339 | |||
| e3217eaf22 | |||
| 540515ab41 | |||
| e1c02a6138 | |||
| a508576785 | |||
| 84e57d5c91 | |||
| 62b1005dbf | |||
| 04a5450a41 | |||
| 2ac4dd6c5a | |||
| 441a569f30 | |||
| f319201e74 | |||
| 47b2fa60d8 | |||
| 4d15c15b2d | |||
| 3702094d98 | |||
| 6185ca595d | |||
| 358b93029b | |||
| 38044b1d2b | |||
| 90877738b3 | |||
| 477613d21f | |||
| a77ee0fa70 | |||
| 036fa444c6 | |||
| d83e1e413f | |||
| 19986099ff | |||
| f23dd6a662 | |||
| 951d306765 | |||
| 8adfaa88a3 | |||
| fcb76e71e5 | |||
| 2da536f667 | |||
| 2034b1c587 | |||
| ea2e169199 | |||
| 2cf6c2747c | |||
| dd24b6243a | |||
| 81fc6aa6cb | |||
| 8e3bcf6b5b | |||
| 8e181c9846 | |||
| 7a737da328 | |||
| 5545c112b7 | |||
| a6f187d444 | |||
| 3ad72ee1f8 | |||
| 2cc100b887 | |||
| 761c5dc0bf | |||
| 80d3f140f4 | |||
| b72621ee2b | |||
| 7896f3ee46 | |||
| d33c15e47b | |||
| ddcd3f8169 | |||
| 42f66cd543 | |||
| c0efd4a8e4 | |||
| a4df780a8b | |||
| e8ac144454 | |||
| d543181b92 | |||
| 02cfb7515b | |||
| 4b71e44c51 | |||
| 68f7e1fa58 | |||
| 2f66588455 | |||
| f39cf38313 | |||
| 1141604d20 | |||
| 6b575f0169 | |||
| 6428179c54 | |||
| 44445e6e34 | |||
| fb48316069 | |||
| ea5312d606 | |||
| 5cb6f9e296 | |||
| fdb1d255ba | |||
| 153b19c966 | |||
| ba79d009e6 | |||
| 47f9c967b1 | |||
| 97ab6108cf | |||
| 8e9da6e1f5 | |||
| aca29abde6 | |||
| dac3f7e2d7 | |||
| 9f9254a835 | |||
| 58e3ef83e0 | |||
| 76bb5d1052 | |||
| afff3446c4 | |||
| 2fa6e9c549 | |||
| cb4bcedba7 | |||
| 5dbe10a8a8 | |||
| 13ca69c569 | |||
| 19e18226bd | |||
| 6b51a198c2 | |||
| 5ad38457af | |||
| ee3043875a | |||
| 92c8a698f3 | |||
| 41f78c365b | |||
| 4b8053beee | |||
| 8008266389 | |||
| 1ce04241f6 | |||
| a37701f148 | |||
| add1339f4a | |||
| f29b8da6d0 | |||
| de968a4820 | |||
| 859a2a5672 | |||
| ddddda10a5 | |||
| 30d6dabe4f | |||
| 7655cc25be | |||
| 52824e5baa | |||
| 014c80cd18 | |||
| a7a48ccf77 | |||
| 99eddec84a | |||
| 6b382ab080 | |||
| d492cfdace | |||
| 256f0af0e9 | |||
| 58d2290e12 | |||
| 547e6da027 | |||
| c3e34ba644 | |||
| e9583284d0 | |||
| b670a9c1c4 | |||
| 077e17c5aa | |||
| f264875b7d | |||
| 351b8a331c | |||
| 479f7c464b | |||
| e04a7fc4b9 | |||
| 595f7747f4 | |||
| 8c1343fdba | |||
| c3717869bd | |||
| 1e56dc533d | |||
| 1444531830 | |||
| 4e759e446b | |||
| 33d86a7995 | |||
| ef6c09d27b | |||
| efc241429c | |||
| 07be540ba1 | |||
| 50479d2e87 | |||
| 2934c9dc38 | |||
| 30a60797e6 | |||
| 53562297d5 | |||
| 144ee3ea8c | |||
| e0c8697e75 | |||
| 6aba2906f6 | |||
| 7bb468a1b1 | |||
| 871be728bf | |||
| bbac0c8d93 | |||
| 6b361ed077 | |||
| 1cfa54f8e8 | |||
| 80ce5052d9 | |||
| 5bd028cbe9 | |||
| b8f8711ba4 | |||
| ec805653bb | |||
| c1f899f0d5 | |||
| e4fd74da7d | |||
| bf9cc93daa | |||
| 3b974268a9 | |||
| 822853a4be | |||
| f699841427 | |||
| c700910b15 | |||
| 3c8d0cb3ec | |||
| 8492c762b5 | |||
| b2d70392bb | |||
| eb9cb08624 | |||
| a4ca2628dc | |||
| 727d78d0e5 | |||
| 693d8c4385 | |||
| d46e3fac2d | |||
| 7f0ec7f128 | |||
| 73eb2f6ecb | |||
| 6bd7ced28c | |||
| 086c31a7d4 | |||
| 3a839f6770 | |||
| b67c7e2803 | |||
| b7dbf30845 | |||
| ec86970cbe | |||
| 956755b985 | |||
| fe2779196d | |||
| 9eeeba93d0 | |||
| d7df019d6d | |||
| 68bfc26ddc | |||
| 94a1cfe4b2 | |||
| 936c3b00bb | |||
| b34229e4c4 | |||
| b51322b473 | |||
| e0657ccdcc | |||
| 4b67d8b5aa | |||
| 5b8cc49349 | |||
| 612f71e18c | |||
| 6239ccb8ab | |||
| 9680bea412 | |||
| a21d9096c8 | |||
| e6f3ad5cc2 | |||
| c41e996add | |||
| 58f230038c | |||
| bda41d8a26 | |||
| e3f41543a4 | |||
| 78c4d7acef | |||
| 8c884b6d23 | |||
| a5f9af2df2 | |||
| e2fd6dbf97 | |||
| e430b4e2de | |||
| caa3f46c5c | |||
| b2beba4d36 | |||
| 3a9c7fc71d | |||
| a5f8275f64 | |||
| 8fa3f3d832 | |||
| 97472b8259 | |||
| ee07889b30 | |||
| 8582860116 | |||
| b8903e7814 | |||
| b47bcfc2c3 | |||
| 35a987a835 | |||
| 6f413b27d4 | |||
| 92173b4794 | |||
| 89e79154aa | |||
| de9145cd70 | |||
| 8bf509ee2d | |||
| d7144265e1 | |||
| cc6c759658 | |||
| 8e5d482f9e | |||
| 5ed8f2499a | |||
| a8c9bcc1b0 | |||
| 21aabcc561 | |||
| a2b86a9e21 | |||
| 74882f56ee | |||
| 1eb9ec1dd3 | |||
| 9e95c0cc7e | |||
| 877238d2d5 | |||
| 9c6f69b16a | |||
| 64af456182 | |||
| 1a84477700 | |||
| a0b6285596 | |||
| 951c139062 | |||
| 99efcd6bbf | |||
| 3482f58b1b | |||
| 6b4f751a16 | |||
| 3be1308230 | |||
| b7710cffa3 | |||
| dc4fe89521 | |||
| c33eb6829a | |||
| e1793d57eb | |||
| cc4c48f718 | |||
| aa20ed9744 | |||
| b6d8688a40 | |||
| d1da2bb7fa | |||
| 7828bce732 | |||
| 026a8d7093 | |||
| 6a9a29c5ff | |||
| 16b4b6cc81 | |||
| 863e570b61 | |||
| c9e8408804 | |||
| 31984f1737 | |||
| ad2a0debdd | |||
| 4c45657092 | |||
| 5d7fc94d51 | |||
| 641b430fe5 | |||
| e1bb02f4e0 | |||
| 36fd8cf408 | |||
| e48b47b315 | |||
| 46935e4cad | |||
| b8a6b4baea | |||
| 8a8f0a95ed | |||
| cdbefd2d4f | |||
| 7e79b4e328 | |||
| 11dc9f9be0 | |||
| ec255099eb | |||
| 64738dceaf | |||
| 7d518696f0 | |||
| fcd97b66df | |||
| 47f5c3b7c1 | |||
| b64fee9d25 | |||
| 498efdf5ea | |||
| 59d6450ded | |||
| 7da206af7e | |||
| 8bd669e11a | |||
| 62570aed9e | |||
| bfe9c04384 | |||
| b27d007d47 | |||
| 7e50e6570d | |||
| 336d449889 | |||
| 3dfea1e6f9 | |||
| 3c1780d9b5 | |||
| 35ee7cf03d | |||
| 72e8c18f9c | |||
| f3f876d213 | |||
| 398907db90 | |||
| 5dd251eaa8 | |||
| 2c73e55f43 | |||
| f4406d7960 | |||
| 3e0a620ac5 | |||
| 9ea8c32608 | |||
| e2ea197f9e | |||
| f92038b5bf | |||
| fa9eee2c4a | |||
| 3e62fb61e1 | |||
| 5d71e0ccc1 | |||
| 4699923058 | |||
| 9206938938 | |||
| 46ac569f70 | |||
| b90d1b4f38 | |||
| 592b7bbc5e | |||
| 3ccb8b3772 | |||
| 422abe1b87 | |||
| a14f35a4f0 | |||
| 3bebc92106 | |||
| 21413cf250 | |||
| 04a34af370 | |||
| e1e6924b6e | |||
| fbd3a42e81 | |||
| 80a2cbb8cd | |||
| 96fc6cc183 | |||
| b5ff891db1 | |||
| d2171e7a3b | |||
| c3a06f57b1 | |||
| 18f41743bf | |||
| fcc647a1f8 | |||
| e80b198aa5 | |||
| 52efa96e2c | |||
| 816dd60298 | |||
| a181292f80 | |||
| 1b946c325c | |||
| befb323721 | |||
| e62c0aaaad | |||
| 4ecc0c073b | |||
| faa41248eb | |||
| 6dca13e80b | |||
| f3fab1b3f6 | |||
| 5340a41298 | |||
| 553ebc137b | |||
| caf2a379d9 | |||
| c59b79427c | |||
| dd2338021e | |||
| e54fcca53e | |||
| ad416c6a5d | |||
| 4a65c1a5c8 | |||
| 878529f646 | |||
| d01bc1e2d1 | |||
| d63f5f5ab2 | |||
| ad0fb7a55a | |||
| b0258eef77 | |||
| 86d4df8ad9 | |||
| bb11d984f9 | |||
| d3f3d2ca98 | |||
| 0c1e712c79 | |||
| e193a40651 | |||
| 03f36476cb | |||
| a721637be3 | |||
| 722fb1ad64 | |||
| 52a05c3aa4 | |||
| bcbdac6afc | |||
| a9507af3f4 | |||
| 68601ca8be | |||
| a7adc27896 | |||
| d19d0eb571 | |||
| d8e365fe08 | |||
| b34f61ce0d | |||
| 1f24d16f95 | |||
| 738074ec00 | |||
| c8eee33475 | |||
| badf9f9c20 | |||
| 0398cc4ffc | |||
| b88abf6b9a | |||
| a0d193bc52 | |||
| 6ac635f8c6 | |||
| 30c7d71114 | |||
| 0223b3ab22 | |||
| c9663662d5 | |||
| 12bed97638 | |||
| a857af970d | |||
| 7da621583f | |||
| 46a32b62c2 | |||
| 45bcc95e7d | |||
| 7d7bcfaa1d | |||
| 3a9a561c77 | |||
| e8d344256a | |||
| 1a7c1119bf | |||
| 0b6a2503aa | |||
| 459b9f65a9 | |||
| 6c1ebe531e | |||
| 604450292b | |||
| e42361e84a | |||
| a974652f7f | |||
| 309ab54e90 | |||
| 486d680c26 | |||
| 61a5f3a275 | |||
| c865417a4b | |||
| b142feaaae | |||
| 7380854133 | |||
| 8a0f185eda | |||
| bd9604a53e | |||
| 1aeab51f94 | |||
| b16a6fedc6 | |||
| b97c381ed4 | |||
| acb8c723a3 | |||
| 4492307e23 | |||
| e31224b332 | |||
| 6933d6c590 | |||
| 3d58841ce5 | |||
| 0654b0a25f | |||
| 44dde3f91b | |||
| abad30595f | |||
| c4df411560 | |||
| 2974fa4562 | |||
| b086b1cb0c | |||
| 22cf026335 | |||
| 90bd53fe61 | |||
| 9c580a356e | |||
| 7198c28e6f | |||
| 7bf074fddc | |||
| bea1a336e9 | |||
| 97a9e03192 | |||
| 244d57b6bc | |||
| 3cbd484147 | |||
| 8b81819c30 | |||
| 1873ac5d4e | |||
| cf70c1e7fe | |||
| 6f6b26ea4d | |||
| e2708d9078 | |||
| fb5a40c6e4 | |||
| e2a10c1410 | |||
| 5461d3d548 | |||
| c47365e626 | |||
| 3476f8df3a | |||
| f812c1e5c8 | |||
| 391f196005 | |||
| 097dbca26e | |||
| a147118381 | |||
| e6bdde8273 | |||
| 747757faa7 | |||
| 6f21b9d0ae | |||
| e505554aac | |||
| 3323cce890 | |||
| ffd468db1e | |||
| e98e0d1522 | |||
| 13a4f2014c | |||
| 8cbfe6450a | |||
| bcc866888e | |||
| 60911d5dcb | |||
| 7e77d5749e | |||
| 6c3be5627d | |||
| f6a9585700 | |||
| 06fddb7dcd | |||
| 3a3f9a625b | |||
| 3702b104fc | |||
| 3e023089a1 | |||
| 7455674f6b | |||
| c93c0f2fc6 | |||
| 727f02e571 | |||
| 784fae1f5d | |||
| 42ef075912 | |||
| 15307c5223 | |||
| aad3b48883 | |||
| d65d8f1c4c | |||
| c581496975 | |||
| d5481a8888 | |||
| e0a61278fc | |||
| 2c36283833 | |||
| 1b5a8f3a7e | |||
| 95c918b4e3 | |||
| f32cc1673c | |||
| f0fc2f06da | |||
| b6c6abaa5b | |||
| 6ed67c911c | |||
| 1eda223a1e | |||
| 316c0c28ab | |||
| 895d4d5cf1 | |||
| 877df95e02 | |||
| 801f0b95e7 | |||
| 9019f555b5 | |||
| 106b03a316 | |||
| d8faa554be | |||
| 93080a74a7 | |||
| 3ea2ede0cb | |||
| 1242848b6f | |||
| 24d44a2c90 | |||
| 971779a529 | |||
| 14f561c237 | |||
| 3a38e746f6 | |||
| dd3bc9d39d | |||
| 39a8062aef | |||
| 55b6ccb760 | |||
| a8a55eb9bd | |||
| 3eb73439aa | |||
| 466e118579 | |||
| b38032074b | |||
| 9ee771e528 | |||
| 9253ed47e6 | |||
| bc82263286 | |||
| 11979240ab | |||
| 255e6182a9 | |||
| f170abb7ea | |||
| eb80deb413 | |||
| 10ba5a9ba5 | |||
| 94c49cba8b | |||
| 354dca8b04 | |||
| 034488ff34 | |||
| 59c70e23dd | |||
| 7d98a842f1 | |||
| 5f8006dc5a | |||
| 239bd69580 | |||
| bc96f102a1 | |||
| e2ef3f4d01 | |||
| 5c20bbf5e4 | |||
| b07edd256a | |||
| 1478d37889 | |||
| 7151b56de3 | |||
| 220fd9528b | |||
| 1c26c35571 | |||
| 78eb6b7b02 | |||
| 9dc9add896 | |||
| 2d09f8c008 | |||
| 166e1e77ec | |||
| ea782d002b | |||
| f525a3c46d | |||
| bef6cbb212 | |||
| 5e0af8654a | |||
| 032a89e0cd | |||
| c9afb6df02 | |||
| b8092447ff | |||
| bff20bea49 | |||
| 87f2d9c85f | |||
| 154dfc8538 | |||
| 59c4176983 | |||
| fddcdfb3aa | |||
| cdbf7d39a5 | |||
| 6a55821d4d | |||
| a305db7b13 | |||
| 58b1cd4b12 | |||
| 5e7559e43f | |||
| 2a74e35388 | |||
| 5b9bef79da | |||
| e89750c364 | |||
| 9da6cbf097 | |||
| f83f719283 | |||
| 4a1c81ffb4 | |||
| be26f5168b | |||
| 4e6c75995c | |||
| df693ce0c2 | |||
| f0236d7ad5 | |||
| 1d3e2b5c16 | |||
| 545d257135 | |||
| f5164d2102 | |||
| 9af83be9a7 | |||
| 49b0b982f5 | |||
| ad7543e7bc | |||
| 1bd0db013b | |||
| ef6a2c58ff | |||
| 2aa41661a2 | |||
| e63a374da1 | |||
| e326f072d2 | |||
| f53cc18ab6 | |||
| 723a504d7c | |||
| dab48607e8 | |||
| 3ee69146bb | |||
| ca86a66b14 | |||
| c92bdd3014 | |||
| 0be23f26f2 | |||
| 6ce4592e5e | |||
| 92caedb011 | |||
| f796411fa8 | |||
| d45f185f77 | |||
| 6564de8a72 | |||
| 10028dfebc | |||
| fa06795cef | |||
| c1eb324d79 | |||
| cdf9528583 | |||
| 5f5a621bd1 | |||
| b8d54dfa59 | |||
| b4f39d09e4 | |||
| d57ac57f43 | |||
| a3aea6259c | |||
| 73e4c90956 | |||
| c1432159f3 | |||
| 45f47d98ba | |||
| 2e9638b8c5 | |||
| 7ea30c1d0e | |||
| 034e04944a | |||
| 99326596d5 | |||
| 1cc2b85816 | |||
| 942291d7c5 | |||
| f4cd9419a4 | |||
| a24b9d92c7 | |||
| 8e7e83ad72 | |||
| cd810f0048 | |||
| fa050039cd | |||
| 935fb1149f | |||
| 951817455a | |||
| de597bdd36 | |||
| e71e7f6163 | |||
| 09be5e157d | |||
| 1f5e59fc1d | |||
| e58861afa4 | |||
| 44e51ecb0a | |||
| 794c17a1bb | |||
| 1b287962f0 | |||
| 8bdf98b9d8 | |||
| e1aeb61c9b | |||
| a39a70ac2d | |||
| 4fdcaaf591 | |||
| b4502638ff | |||
| 05502d56b1 | |||
| 90e1e8a40f | |||
| b10293da50 | |||
| 83cf0687e6 | |||
| f56b03716a | |||
| 61a41e6039 | |||
| 20ae9fe0ec | |||
| 74fdec9d72 | |||
| 3437265cc1 | |||
| 271993a876 | |||
| 3a8b7bb920 | |||
| fd85f3889d | |||
| 556ecea749 | |||
| 8a97844676 | |||
| f152296e7a | |||
| 4ccc789c7c | |||
| 747b02eb7a | |||
| a73c033c03 | |||
| 939db8c820 | |||
| 8601440d97 | |||
| 7a1fa90175 | |||
| 46722ba69d | |||
| 70d9d461bf | |||
| 91944df6a4 | |||
| ef86c1158c | |||
| 0f3b6ed34b | |||
| 8b50620ddc | |||
| 8b2a9eb6ca | |||
| 7297f5480e | |||
| 3bee8cc034 | |||
| 584a16e111 | |||
| 6460c7f8d6 | |||
| feb99c9f78 | |||
| 68ac809bcb | |||
| de207f66d9 | |||
| 054fcd2049 | |||
| 497fc998fe | |||
| 03f76453ab | |||
| 0a87bd354a | |||
| 1baceaef15 | |||
| 9719a7fa28 | |||
| 1718a66126 | |||
| b317ef3a39 | |||
| 8b0cd69ae6 | |||
| c7126e9836 | |||
| f3d01335a4 | |||
| 88e029b129 | |||
| 2bc72328c1 | |||
| 81179b1f72 | |||
| 9a7d4997c2 | |||
| 3b6ac881c2 | |||
| 6b5fb7d8bc | |||
| 1551b0a358 | |||
| 6540af8386 | |||
| 9badcdc382 | |||
| 0ba94fa56f | |||
| 52c1343ade | |||
| 7a0e633b79 | |||
| 9b68b05d7d | |||
| 9e7f6b0854 | |||
| cf20ad6fc2 | |||
| cb7bdba338 | |||
| f1fc06ca84 | |||
| 18f9fe7fcf | |||
| 18816a8a4e | |||
| 8d379501cb | |||
| 8f4c6abfd3 | |||
| a24d0a9618 | |||
| 8cdd66cd89 | |||
| 3fd34576e8 | |||
| 78d5cbcc42 | |||
| 803f2bef75 | |||
| 40c7e8f9e6 | |||
| b3c5ca6112 | |||
| 31067ea66d | |||
| 4e7626ff41 | |||
| e62822505e | |||
| cbc705d1eb | |||
| aa2e147a51 | |||
| 6249726839 | |||
| 8f4bc5a164 | |||
| eee459d08a | |||
| 81b4e40dbf | |||
| 261068e286 | |||
| 84364a7c66 | |||
| 3beb47a8be | |||
| ca76af4474 | |||
| bc33673533 | |||
| c5d038a173 | |||
| b1e492df1b | |||
| 9d9c213af7 | |||
| d3f97ea527 | |||
| 0a5fb4cb1d | |||
| 6905f7191a | |||
| bad7fb4922 | |||
| 250fa7eb7b | |||
| f0cd8567cb | |||
| f6dd35e4b8 | |||
| 2dc299e7f4 | |||
| 366e8ded14 | |||
| 8175742143 | |||
| fc858f1272 | |||
| 84b668714b | |||
| 5ea5346ee8 | |||
| c2eb0b267c | |||
| 7fb502e87e | |||
| 98726ddc3a | |||
| 75ff76acf4 | |||
| ba552812a3 | |||
| 83bfeb0abc | |||
| 70f1b7a678 | |||
| 455d53fee0 | |||
| 39e4b5bf55 | |||
| 40a729b6f8 | |||
| 9643176e06 | |||
| 1a70c33bef | |||
| 888ebe5f54 | |||
| cb02dbae57 | |||
| 2683d02dcd | |||
| ceb924e8f1 | |||
| bd91609a80 | |||
| 2fd74a4698 | |||
| e9e0d3b43e | |||
| 17e09ddad3 | |||
| 65427c55d6 | |||
| e1fc23a1bb | |||
| 2d551a3f73 | |||
| 7f99f75c6f | |||
| 548aea8d13 | |||
| a177137744 | |||
| 3c6443d78f | |||
| ba81ed9cb0 | |||
| 6a1cbd10c6 | |||
| 9b205366f7 | |||
| 7e9ac0c4f1 | |||
| fe889639b3 | |||
| 848e43af28 | |||
| c7a3893fae | |||
| f7d633188c | |||
| e1e7d2d3d6 | |||
| 2b8a280768 | |||
| 6efe96eb0d | |||
| 3562fe9273 | |||
| 739ef44a8b | |||
| 879b42dbf2 | |||
| fae626bb98 | |||
| cebd639d78 | |||
| 17a99a1cda | |||
| 2a03683e1e | |||
| a579b3fe10 | |||
| 9c75dd18df | |||
| cdc9c86852 | |||
| fab1851436 | |||
| a6704e46a9 | |||
| e188f70eb6 | |||
| 02dd115886 | |||
| 5cbfc7b461 | |||
| b4742b5645 | |||
| 5ec25475ea | |||
| c58040ef83 | |||
| 75701b6875 | |||
| 2155a33689 | |||
| 68a9d5d771 | |||
| ebf6107faf | |||
| e4bc36a743 | |||
| 7a64251811 | |||
| 65409d75a7 | |||
| d40081d58b | |||
| f276e981ed | |||
| e50db66b47 | |||
| fc84022852 | |||
| b593f2f3ea | |||
| e782d0542c | |||
| 1a085cad98 | |||
| 1c33a0c4c5 | |||
| 3c4a7961c2 | |||
| 712f9b84cf | |||
| 2efb7b76cc | |||
| 5c4d93ce15 | |||
| 7addd92058 | |||
| 82c5898b9a | |||
| cced6b7035 | |||
| ae0b5b3738 | |||
| 6fdf9cbe5d | |||
| 009244c65d | |||
| 73a720bb9c | |||
| fd9df9904f | |||
| e75fb3a40d | |||
| 0bef1a2aa8 | |||
| 395eb641e5 | |||
| 9019242ffb | |||
| 9433bb72ca | |||
| 500f751152 | |||
| 9ebe3f4a0e | |||
| 4727f22b0f | |||
| 8c92fc9a42 | |||
| 2a7cb34218 | |||
| e1a42b49c1 | |||
| 5a825debf5 | |||
| e46b0a42b0 | |||
| eb0c442a5e | |||
| eb6460236b | |||
| ef051daffd | |||
| b6acb302d2 | |||
| 5cfc5a3971 | |||
| 04de97af16 | |||
| 6367f90589 | |||
| bc8f9d07bb | |||
| a3d693ddc1 | |||
| e205abd120 | |||
| 5da8fccef7 | |||
| c7e78142ee | |||
| 79cbb44d51 | |||
| dcde3db33a | |||
| 44fbc7a182 | |||
| 354c7d1f85 | |||
| d7a85edb76 | |||
| ceb9dc6707 | |||
| b0f20ee017 | |||
| cdf78f2bc5 | |||
| d3f0a8fe4a | |||
| 17acf1bd86 | |||
| 31a0bb24c7 | |||
| 13ff1f4343 | |||
| ec76be9d5e | |||
| 84d53616a0 | |||
| 2758216ae2 | |||
| c492f066ca | |||
| 897478e30e | |||
| a94d825ead | |||
| ba05e6137e | |||
| dc55342343 | |||
| 3ea96d27db | |||
| 48842c099b | |||
| 4f87cac46a | |||
| 52680b63b9 | |||
| f89defc78f | |||
| 46e9a161a4 | |||
| 8e602e8169 | |||
| c60df98577 | |||
| ad7db50336 | |||
| e40ef5292e | |||
| 94b844788c | |||
| 9c222baf91 | |||
| 0794606e57 | |||
| be34c486bf | |||
| fa7a5fef9b | |||
| 9e518d5414 | |||
| 2d17ecd438 | |||
| 56d6c28811 | |||
| 86b77e29e2 | |||
| 23b8b09834 | |||
| 30eb397c66 | |||
| 05bcc0f818 | |||
| 3f9434239f | |||
| 4caf7eabc0 | |||
| 0aee08bbb0 | |||
| 602dbff3c8 | |||
| d52cfd475f | |||
| 4512accb37 | |||
| 2d57c00149 | |||
| cae908da6a | |||
| 38f97673b8 | |||
| ed2bf89413 | |||
| 259d343e31 | |||
| 8e9dbafb4d | |||
| 7e737d7a46 | |||
| 50fd30d173 | |||
| 4c7310f71e | |||
| b3e7168922 | |||
| 261cc5b0eb | |||
| 60218eea97 | |||
| bc1945326a | |||
| 2fd12d56ed | |||
| cb28838b40 | |||
| 4cfd6952c0 | |||
| f5761e378e | |||
| 314144c384 | |||
| 81debc1cd8 | |||
| 9a2baf1d8c | |||
| 55ec26bd3d | |||
| 4c372eeb2b | |||
| c2f9e28edd | |||
| 65a8297fe0 | |||
| 1a48cca197 | |||
| 24984b5d67 | |||
| 619660e5e5 | |||
| c9c4a996dc | |||
| b8ae8b68dc | |||
| a9f4620d8b | |||
| 66abdb1eed | |||
| 1dc4be2d54 | |||
| a286f51801 | |||
| 27708da1bf | |||
| 3aa0e19b0f | |||
| 4d5627348a | |||
| 2aefeb5e53 | |||
| 728b663c7a | |||
| 36156cddc4 | |||
| 2b98702ceb | |||
| 2c2ab02398 | |||
| 8ae8c6d6fb | |||
| 1171424f19 | |||
| 3444922861 | |||
| 5821a519ee | |||
| 439a2353b0 | |||
| 8f8f3193dd | |||
| 5907b8b1a5 | |||
| ba5783417e | |||
| 56573deea1 | |||
| 892933888c | |||
| b3725baad0 | |||
| cf79735780 | |||
| ad059fe18d | |||
| 03fa2052da | |||
| d7d55b7341 | |||
| 6ea9a7de90 | |||
| 3bc65f42c7 | |||
| 854227304f | |||
| 62d34c6c06 | |||
| 69f0beff9c | |||
| 2a32859c48 | |||
| c6bdb4ee8d | |||
| cf5e981758 | |||
| 2bf246562b | |||
| c1cd25b89b | |||
| 2eb259055a | |||
| fe743590c9 | |||
| d3de1c238c | |||
| 931593d726 | |||
| 8f002fc804 | |||
| 10e0d0123e | |||
| b93cb06662 | |||
| 5471099e7b | |||
| 5a2f86be87 | |||
| 3d7ce17b07 | |||
| cf0e237529 | |||
| 60c84153e5 | |||
| f9a8efe084 | |||
| 6b48c546c4 | |||
| b1893718f6 | |||
| 7ad95d5f26 | |||
| 778b0e8f16 | |||
| ad847f6113 | |||
| 27778c2b31 | |||
| 484694cbdb | |||
| 969680a7ad | |||
| 6357e008dc | |||
| a570cfe32b | |||
| ccba561d47 | |||
| 0dcf86ce1c | |||
| 961d4ebb5c | |||
| 54b947504c | |||
| f5abd7e075 | |||
| 35cd97c751 | |||
| 147bc150f5 | |||
| ee7f102d34 | |||
| 7f0e59aba5 | |||
| 135b55f6f9 | |||
| 6986bd9a57 | |||
| 17782a500f | |||
| 64a64960ac | |||
| d4be850e68 | |||
| a6feb57f85 | |||
| 50fee01249 | |||
| 4ff12e91aa | |||
| 24b196f216 | |||
| 414064699a | |||
| d36b2f99ee | |||
| 704b73aef7 | |||
| 99878606d5 | |||
| e1cfccfcc0 | |||
| 4c2a46875d | |||
| 6ce64892e4 | |||
| 92e1c55913 | |||
| 72cfcd66b1 | |||
| 0cb65ab216 | |||
| d18731ccfe | |||
| b0a4aa66f6 | |||
| aad92e529e | |||
| 6e03e75110 | |||
| 91f79a5ff1 | |||
| cc53a1f3d6 | |||
| 25cd086298 | |||
| b27ecab969 | |||
| db1c0d954f | |||
| 03d20aed61 | |||
| d37929548e | |||
| 663891dd54 | |||
| 79d1c4fed4 | |||
| ecc8196701 | |||
| c2fb1a58db | |||
| 55998c3c4d | |||
| 83e44808cb | |||
| bfbc9900ed | |||
| 1735ba3312 | |||
| 1ef8fa5deb | |||
| d0a2868e5a | |||
| 49c2366a89 | |||
| cb7d985195 | |||
| bead0bded1 | |||
| cc92d5f639 | |||
| 84c281122e | |||
| bb4a35a6a9 | |||
| 71ae36fbf0 | |||
| 04864e059c | |||
| 7a47184af3 | |||
| 48e2831e96 | |||
| c1caa23b10 | |||
| 443cc6007d | |||
| 7d99923960 | |||
| 7342d0706f | |||
| c15c194feb | |||
| 3b6a5b5a20 | |||
| f9028ea8a5 | |||
| 8cd06a80ba | |||
| 6c80266abb | |||
| ca3c1fa8c2 | |||
| 2b7deaef2c | |||
| 42f1898bd1 | |||
| 0bdff3eac7 | |||
| a00f6660a1 | |||
| 67d1ad497f | |||
| f1bbf4e9fc | |||
| 00ff5e6a94 | |||
| 7f77a8fd63 | |||
| 7c1267148b | |||
| 2dbb7b67cb | |||
| c41db52b05 | |||
| 72f3a262ca | |||
| 2fbe6a4937 | |||
| c6fb63afe6 | |||
| 2e05320ddf | |||
| a3f86ab604 | |||
| 5d2efda925 | |||
| 4a30ac04b7 | |||
| 1097068393 | |||
| df88502c3d | |||
| b8df485a7e | |||
| a8a66a7b1e | |||
| a1548faeda | |||
| 34f452be6e | |||
| bef04854e3 | |||
| 757782e43b | |||
| 8a69f565a3 | |||
| be20d1fec6 | |||
| 7a71e457ef | |||
| 3af9fa0a10 | |||
| ff8b3efde9 | |||
| 161f63a664 | |||
| 3206e04639 | |||
| f1dea0f200 | |||
| d94a83a7c3 | |||
| 6bbc7104d9 | |||
| a2180196ee | |||
| a6dc4c7891 | |||
| fa7355c5c6 | |||
| 0140c10f15 | |||
| 796da0e673 | |||
| 743f78096a | |||
| 52d726b6f3 | |||
| bed4450900 | |||
| c2ab369d0c | |||
| 32a7f37b6a | |||
| b299199579 | |||
| 7091ea179d | |||
| dceb582bb1 | |||
| 6e2e9f7dcf | |||
| c27025269e | |||
| cf1411b412 | |||
| b167f5bb8b | |||
| 87d0fd9edb | |||
| b59a169e10 | |||
| 941e3985b0 | |||
| 127fb76c19 | |||
| 5f3028f795 | |||
| b596f04dba | |||
| 0bb96ecf12 | |||
| 60f21f6113 | |||
| 051000e4a8 | |||
| 14e8020147 | |||
| a99b1499a2 | |||
| 6a1c01556b | |||
| c41327640b | |||
| 7df5834ebc | |||
| ebafc2f98f | |||
| 92c9d81171 | |||
| 7d34a3972d | |||
| 90f1254c6c | |||
| 8b71d68749 | |||
| 17c0cf52b6 | |||
| 045be4ff09 | |||
| af7948fcb5 | |||
| e1c12d5007 | |||
| 8676b20fbb | |||
| 54db9fb910 | |||
| 0eaee1ae63 | |||
| e141dfb6b5 | |||
| b14b8c9f66 | |||
| b4b589f307 | |||
| 7a74bc0d04 | |||
| b826c0a9ad | |||
| a197b59200 | |||
| c25503d7f8 | |||
| 2c0449dcf6 | |||
| 59655daa3b | |||
| 1c36d7b923 | |||
| a51175620c | |||
| 7b95c265df | |||
| a11bb9c210 | |||
| 72227a5591 | |||
| f9f2eb4c05 | |||
| e9c57df5fb | |||
| 158c47d94b | |||
| 7616435098 | |||
| 69443ff457 | |||
| 6a257c0a9f | |||
| eddd56e53a | |||
| de6648867b | |||
| 567a6ceda5 | |||
| 59330bcd7e | |||
| 36803f24bc | |||
| 46458e2667 | |||
| d0aa6e1fe5 | |||
| 9bef919123 | |||
| 88a0603d50 | |||
| 106dbb3d8d | |||
| 4214d924a4 | |||
| c3b40f9264 | |||
| 69f00626a2 | |||
| c485b1f7f8 | |||
| 7186c73a64 | |||
| 88f9f65fb6 | |||
| 714573ec6b | |||
| a979445ffc | |||
| 3cf38eb3fc | |||
| 94ceca35bf | |||
| 2bd4eb2004 | |||
| 355e71e620 | |||
| 7d9520d97b | |||
| ce0957cea7 | |||
| c93169fe97 | |||
| 485dd96e93 | |||
| 9b0f0892ff | |||
| 9a04f3ae38 | |||
| 968d668afa | |||
| 85d19ed2bd | |||
| 28e490f8d4 | |||
| 298ae7f657 | |||
| d50f29b3dc | |||
| f167eff21c | |||
| 69d4f00807 | |||
| 904dc4fe2e | |||
| c870cc683d | |||
| deb29fd2e4 | |||
| 19f1bc947a | |||
| 60d1b47c66 | |||
| d67b845dfd | |||
| 6f1c7c7ded | |||
| 46e9118d73 | |||
| 8aa1557b7a | |||
| 6058c3f8c1 | |||
| d409e6f2a9 | |||
| a4d272c556 | |||
| 911d7c04ec | |||
| afc379d449 | |||
| 18feca49e2 | |||
| 792b19d40d | |||
| 108d05edea | |||
| d8882da17b | |||
| f120dfda15 | |||
| dd60ed7abd | |||
| d0b478c743 | |||
| defe69ba18 | |||
| 32d748efae | |||
| f194dc9bf6 | |||
| 075fbd77b2 | |||
| edb3e3713a | |||
| c70648e038 | |||
| 966f4ee57d | |||
| f90ced4c31 | |||
| 6bda4d80bf | |||
| 3ecb6b30c9 | |||
| 3c337b618c | |||
| 40a90bbcfa | |||
| e54b124252 | |||
| 18923efe23 | |||
| 5c5ad63cc6 | |||
| d5fd444ae5 | |||
| 4a22f70e30 | |||
| 9805bd4caa | |||
| 0690f13bea | |||
| da8faea4c8 | |||
| ff927bab09 | |||
| e3e5def3b6 | |||
| ba246585ad | |||
| f242a6a3bc | |||
| 0c89e76b39 | |||
| 1fa82e72f1 | |||
| b4e6b04e47 | |||
| 002f824f5c | |||
| 1aaf7f8de1 | |||
| 82ba19842c | |||
| 32ed1bf622 | |||
| 0d4838c536 | |||
| 11527b46f9 | |||
| 20c1957b59 | |||
| ed2630e016 | |||
| 8cc2ac2cad | |||
| 37745ca548 | |||
| 29edcdca4b | |||
| 92669d6974 | |||
| 351a64f592 | |||
| a29e56543b | |||
| 6db3e8c671 | |||
| 20c4d24698 | |||
| 114e185bcb | |||
| a293bff964 | |||
| 98b4d9f95d | |||
| 8489e87586 | |||
| 6f7ee2179d | |||
| ea6b442a48 | |||
| 12d26edd76 | |||
| 27053eecd7 | |||
| 151d5b9c1f | |||
| d35c558658 | |||
| baa4dc7a23 | |||
| 9b565c32d1 | |||
| 243dd26d73 | |||
| 263234fecb | |||
| 0ba598ee67 | |||
| 29430b9967 | |||
| 2038b16dce | |||
| 9ea793397b | |||
| 6061938129 | |||
| 62277aa525 | |||
| 1c118b06c9 | |||
| a41c315e36 | |||
| 40576e0012 | |||
| a582db52f4 | |||
| cb2656ad85 | |||
| da1c686d42 | |||
| b613fe2ba0 | |||
| 83e29b1cf3 | |||
| 5bbd23bf33 | |||
| 9f69b83b51 | |||
| 4f77781e24 | |||
| b23d589a29 | |||
| 7f16a69301 | |||
| 006da73737 | |||
| 3f998cdf75 | |||
| 3e94cd9d8c | |||
| 5922f92caf | |||
| f5f5909fde | |||
| 7e67357887 | |||
| d5750c6cb8 | |||
| b2983d5c96 | |||
| 5a19e46d26 | |||
| 8456a5a1d6 | |||
| b9329a092e | |||
| 6ecad9eebd | |||
| f32dd9efee | |||
| 48cbaf03b7 | |||
| 7f23cda97a | |||
| 49c1809aa2 | |||
| 173aa82134 | |||
| c57e8b2c76 | |||
| bb741013d4 | |||
| 5b014a5a1d | |||
| a85548ac33 | |||
| 77e689fec4 | |||
| fe942dcf73 | |||
| da3b700403 | |||
| fcd46cb2ea | |||
| e918b19e2e | |||
| afb8badc74 | |||
| 4e22a5c7bb | |||
| b5d2e02606 | |||
| 28afb4f14c | |||
| 165659a664 | |||
| 7b3d910b10 | |||
| 1e4a85719a | |||
| 4269cb37d2 | |||
| b7ce3e8c20 | |||
| 15a0240808 | |||
| 0aa68ea04b | |||
| 2d9b69fd1e | |||
| 48d6023e0d | |||
| 113ebb36e9 | |||
| d0c9bd8038 | |||
| 104586fb7f | |||
| 0612582a46 | |||
| 1bc5bb3ef6 | |||
| a3d0d91dd7 | |||
| 982e8f67bf | |||
| 0bb871bff2 | |||
| 0eb43d6552 | |||
| b4f29f4856 | |||
| 2719bff65f | |||
| 4fd68565d2 | |||
| b87972143d | |||
| 8241551438 | |||
| d7bb45f287 | |||
| 0cdec2a226 | |||
| dbb3078300 | |||
| 545c235b24 | |||
| 53d3cb320b | |||
| 38682f01a8 | |||
| a7d141efcc | |||
| 31c404021c | |||
| 631cec2fc6 | |||
| 13a76e8de7 | |||
| 1e98fa98c3 | |||
| be6eab0cae | |||
| 71ee8bfd29 | |||
| 66c9c02dc6 | |||
| 7880ee4aef | |||
| 6274d1f778 | |||
| 5e98813f92 | |||
| 1a30800fb4 | |||
| 454e6933f4 | |||
| e3072071b7 | |||
| 83a88326f0 | |||
| 27109a810f | |||
| 3d5bd424c5 | |||
| c7cd56e7be | |||
| 860769c44a | |||
| a83761b88d | |||
| a79fc9c0b1 | |||
| 877bd63d9d | |||
| bb9d0582c4 | |||
| df7e89e7e4 | |||
| a12593ea51 | |||
| 5ca0af4285 | |||
| 20bc38add4 | |||
| 89e72a0fea | |||
| efabaafa38 | |||
| fc25ad7ddb | |||
| 6b656cf446 | |||
| 26927661e7 | |||
| 15ba945751 | |||
| 8e0527b777 | |||
| b9dfa0a8a9 | |||
| c462973eb6 | |||
| b8cefd69c0 | |||
| 1c1537be8a | |||
| 1dad632c70 | |||
| 41600c7576 | |||
| 5f0074c88d | |||
| 907f2a2678 | |||
| 4a118bd51c | |||
| 4919b46e5d | |||
| 034a6662f9 | |||
| 3fb1c9f315 | |||
| aed4610afd | |||
| 345be8035a | |||
| 68c7b0ddaf | |||
| f39ce00152 | |||
| 0b6d1bd6df | |||
| dca7a44884 | |||
| d90cdf341e | |||
| 7828189ea0 | |||
| e4cd8da65c | |||
| 8cebbd3bbe | |||
| 63d9b354d4 | |||
| 254944fbfc | |||
| ec93080190 | |||
| ec638cb231 | |||
| 7c72623f90 | |||
| 2e58404e01 | |||
| 5eae896330 | |||
| 6c2dcc7d37 | |||
| d40e4e97c1 | |||
| 3164ed1b0e | |||
| b49fae0384 | |||
| 80544aff32 | |||
| 64235951c2 | |||
| f08fe7d855 | |||
| ff1a9e8694 | |||
| 2abf87628a | |||
| e835482abd | |||
| e78882e911 | |||
| b021d99f78 | |||
| 084781dc0a | |||
| ce83fed40d | |||
| 24d16a234f | |||
| 536af50a4f | |||
| 9eccc459d9 | |||
| 6e8310e4c9 | |||
| 35e79f938c | |||
| 10764ac3f4 | |||
| 2a6ecfa176 | |||
| 5b44c7f856 | |||
| 1e5e36a8c7 | |||
| 92dda8e64c | |||
| 27488e8084 | |||
| 72871e55c6 | |||
| 07c05b2191 | |||
| 89d2fb98fa | |||
| 6d1719b877 | |||
| 51599d5bb4 | |||
| aedaee8d3a | |||
| c1c655fcb0 | |||
| fabcce9a6c | |||
| 16feb963f6 | |||
| e7ae608986 | |||
| 7ec4625d45 | |||
| 135d717715 | |||
| 77c3131a69 | |||
| 7e74cbfbd1 | |||
| a2cd548d1e | |||
| 0a4d6122df | |||
| 23d1435ed2 | |||
| dd60900bcb | |||
| 0fbb0aa363 | |||
| 23539b9316 | |||
| a1460b07c6 | |||
| 02b6c2733b | |||
| 1d29d68aa2 | |||
| aa76a20065 | |||
| ec945919ba | |||
| e626e82945 | |||
| 6e238379f2 | |||
| 7a4ae83a2b | |||
| 46827b1489 | |||
| 762897521b | |||
| b8d74357b5 | |||
| 21c116ea5b | |||
| 2cf23b9a32 | |||
| 6b03098ac7 | |||
| bcd846024e | |||
| d98edc8f77 | |||
| faef2a5e4d | |||
| 3d88792d11 | |||
| f7fe7a5153 | |||
| 9aeb1624a6 | |||
| ec9c282618 | |||
| 7e1da213d5 | |||
| 8d9fd482b9 | |||
| c87bf1e613 | |||
| 81dc17530f | |||
| b53f769a0d | |||
| aadd2071b9 | |||
| fc7566d2c9 | |||
| 7f082b7a36 | |||
| bebda2de70 | |||
| 3e0887b5f9 | |||
| a8894cc464 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,5 +7,6 @@ local.properties
|
||||
captures/
|
||||
build/
|
||||
release-app/
|
||||
test-app/
|
||||
scripts/apk-channel/
|
||||
app/src/test/java/com/gh/gamecenter
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -1,4 +1,4 @@
|
||||
[submodule "libraries/LGLibrary"]
|
||||
path = libraries/LGLibrary
|
||||
url = git@gitlab.ghzs.com:android/common-library.git
|
||||
url = git@git.ghzs.com:android/common-library.git
|
||||
branch = master
|
||||
|
||||
93
README.md
93
README.md
@ -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
|
||||
|
||||
381
app/build.gradle
381
app/build.gradle
@ -2,11 +2,10 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android' // kotlin
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
//apply plugin: 'io.sentry.android.gradle'
|
||||
apply plugin: 'AndResGuard'
|
||||
|
||||
// apkChannelPackage
|
||||
apply plugin: 'channel'
|
||||
|
||||
apply from: 'tinker-support.gradle'
|
||||
import groovy.xml.XmlUtil
|
||||
|
||||
android {
|
||||
|
||||
@ -14,8 +13,9 @@ android {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@ -23,11 +23,19 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
dexOptions {
|
||||
// jumboMode = true
|
||||
javaMaxHeapSize "4g"
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
additionalParameters "--no-version-vectors"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
||||
multiDexEnabled true
|
||||
@ -38,9 +46,13 @@ android {
|
||||
}
|
||||
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
// 如果不添加 `arm64` 调用系统的 PackageManager 的方法读取安装包信息的时候会出现 native 层闪退,草
|
||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
||||
}
|
||||
|
||||
renderscriptTargetApi 18
|
||||
renderscriptSupportModeEnabled true
|
||||
|
||||
// 由于app只针对中文用户,所以仅保留zh资源,其他删掉
|
||||
resConfigs "zh"
|
||||
|
||||
@ -61,6 +73,19 @@ android {
|
||||
buildConfigField "String", "WEIBO_APPKEY", "\"${WEIBO_APPKEY}\""
|
||||
buildConfigField "String", "MTA_APPKEY", "\"${MTA_APPKEY}\""
|
||||
buildConfigField "String", "TD_APPID", "\"${TD_APPID}\""
|
||||
buildConfigField "String", "LETO_APPID", "\"${LETO_APPID}\""
|
||||
buildConfigField "String", "TTAD_APPID", "\"${TTAD_APPID}\""
|
||||
buildConfigField "String", "DOUYIN_CLIENTKEY", "\"${DOUYIN_CLIENTKEY}\""
|
||||
buildConfigField "String", "DOUYIN_CLIENTSECRET", "\"${DOUYIN_CLIENTSECRET}\""
|
||||
buildConfigField "String", "QUICK_LOGIN_APPID", "\"${QUICK_LOGIN_APPID}\""
|
||||
buildConfigField "String", "QUICK_LOGIN_APPKEY", "\"${QUICK_LOGIN_APPKEY}\""
|
||||
|
||||
buildConfigField "String", "MIPUSH_APPID", "\"${MIPUSH_APPID}\""
|
||||
buildConfigField "String", "MIPUSH_APPKEY", "\"${MIPUSH_APPKEY}\""
|
||||
buildConfigField "String", "MEIZUPUSH_APPID", "\"${MEIZUPUSH_APPID}\""
|
||||
buildConfigField "String", "MEIZUPUSH_APPKEY", "\"${MEIZUPUSH_APPKEY}\""
|
||||
|
||||
resValue "string", "huawei_push_appid", "appid=${HUAWEI_PUSH_APPID}"
|
||||
|
||||
/**
|
||||
* Build Time 供区分 jenkins 打包时间用
|
||||
@ -89,7 +114,7 @@ android {
|
||||
signingConfig signingConfigs.debug
|
||||
|
||||
buildConfigField "String", "EXPOSURE_REPO", "\"test\""
|
||||
buildConfigField "String", "EXPOSURE_VERSION", "\"E3\""
|
||||
buildConfigField "String", "EXPOSURE_VERSION", "\"E4\""
|
||||
|
||||
multiDexKeepProguard file("tinker_multidexkeep.pro")
|
||||
}
|
||||
@ -101,113 +126,127 @@ android {
|
||||
signingConfig signingConfigs.release
|
||||
|
||||
buildConfigField "String", "EXPOSURE_REPO", "\"exposure\""
|
||||
buildConfigField "String", "EXPOSURE_VERSION", "\"E3\""
|
||||
buildConfigField "String", "EXPOSURE_VERSION", "\"E4\""
|
||||
|
||||
multiDexKeepProguard file("tinker_multidexkeep.pro")
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "nonsense"
|
||||
flavorDimensions("env")
|
||||
|
||||
sourceSets {
|
||||
publish {
|
||||
java.srcDirs = ['src/main/java']
|
||||
}
|
||||
internal {
|
||||
java.srcDirs = ['src/main/java']
|
||||
}
|
||||
tea {
|
||||
java.srcDirs = ['src/main/java', 'src/tea/java']
|
||||
}
|
||||
gdt {
|
||||
java.srcDirs = ['src/main/java', 'src/gdt/java']
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多渠道打包,渠道请参考"channel.txt"文件,所有渠道值均通过java code设置
|
||||
*/
|
||||
productFlavors {
|
||||
// publish release host
|
||||
publish {
|
||||
dimension "nonsense"
|
||||
dimension "env"
|
||||
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
|
||||
buildConfigField "String", "COMMENT_HOST", "\"${COMMENT_HOST}\""
|
||||
buildConfigField "String", "DATA_HOST", "\"${DATA_HOST}\""
|
||||
|
||||
buildConfigField "String", "SENSITIVE_API_HOST", "\"${SENSITIVE_API_HOST}\""
|
||||
|
||||
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
|
||||
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
|
||||
buildConfigField "String", "MIPUSH_APPID", "\"${MIPUSH_APPID}\""
|
||||
buildConfigField "String", "MIPUSH_APPKEY", "\"${MIPUSH_APPKEY}\""
|
||||
buildConfigField "String", "MEIZUPUSH_APPID", "\"${MEIZUPUSH_APPID}\""
|
||||
buildConfigField "String", "MEIZUPUSH_APPKEY", "\"${MEIZUPUSH_APPKEY}\""
|
||||
|
||||
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
|
||||
}
|
||||
// internal test dev host
|
||||
internal {
|
||||
dimension "nonsense"
|
||||
dimension "env"
|
||||
versionNameSuffix "-debug"
|
||||
|
||||
buildConfigField "String", "API_HOST", "\"${DEV_API_HOST}\""
|
||||
buildConfigField "String", "COMMENT_HOST", "\"${DEV_COMMENT_HOST}\""
|
||||
buildConfigField "String", "DATA_HOST", "\"${DEV_DATA_HOST}\""
|
||||
|
||||
buildConfigField "String", "UMENG_APPKEY", "\"${DEBUG_UMENG_APPKEY}\""
|
||||
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${DEBUG_UMENG_MESSAGE_SECRET}\""
|
||||
buildConfigField "String", "MIPUSH_APPID", "\"${DEBUG_MIPUSH_APPID}\""
|
||||
buildConfigField "String", "MIPUSH_APPKEY", "\"${DEBUG_MIPUSH_APPKEY}\""
|
||||
buildConfigField "String", "MEIZUPUSH_APPID", "\"${DEBUG_MEIZUPUSH_APPID}\""
|
||||
buildConfigField "String", "MEIZUPUSH_APPKEY", "\"${DEBUG_MEIZUPUSH_APPKEY}\""
|
||||
buildConfigField "String", "SENSITIVE_API_HOST", "\"${DEV_API_HOST}\""
|
||||
|
||||
buildConfigField "String", "BUGLY_APPID", "\"${DEBUG_BUGLY_APPID}\""
|
||||
buildConfigField "String", "UMENG_APPKEY", "\"${DEV_UMENG_APPKEY}\""
|
||||
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${DEV_UMENG_MESSAGE_SECRET}\""
|
||||
buildConfigField "String", "BUGLY_APPID", "\"${DEV_BUGLY_APPID}\""
|
||||
}
|
||||
|
||||
tea {
|
||||
dimension "env"
|
||||
|
||||
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
|
||||
|
||||
buildConfigField "String", "SENSITIVE_API_HOST", "\"${SENSITIVE_API_HOST}\""
|
||||
|
||||
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
|
||||
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
|
||||
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
|
||||
|
||||
manifestPlaceholders.put("APPLOG_SCHEME", "rangersapplog.byAx6uYt".toLowerCase())
|
||||
}
|
||||
|
||||
gdt {
|
||||
dimension "env"
|
||||
|
||||
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
|
||||
|
||||
buildConfigField "String", "SENSITIVE_API_HOST", "\"${SENSITIVE_API_HOST}\""
|
||||
|
||||
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
|
||||
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
|
||||
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apkChannelPackage
|
||||
channel {
|
||||
//多渠道包的输出目录,默认为new File(project.buildDir,"channel")
|
||||
baseOutputDir = new File(project.buildDir, "channel")
|
||||
//多渠道包的命名规则,默认为:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}
|
||||
apkNameFormat = '${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}'
|
||||
}
|
||||
|
||||
rebuildChannel {
|
||||
// baseDebugApk = 已有Debug APK
|
||||
// baseReleaseApk = 已有Release APK
|
||||
// //默认为new File(project.buildDir, "rebuildChannel/debug")
|
||||
// debugOutputDir = Debug渠道包输出目录
|
||||
// //默认为new File(project.buildDir, "rebuildChannel/release")
|
||||
// releaseOutputDir = Release渠道包输出目录
|
||||
}
|
||||
|
||||
repositories {
|
||||
flatDir {
|
||||
dirs 'libs/aars'
|
||||
dirs 'libs', 'libs/aars'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
|
||||
gdtImplementation fileTree(include: ['*.jar', '*.aar'], dir: 'src/gdt/libs')
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakcanary}"
|
||||
debugImplementation "com.facebook.stetho:stetho:${stetho}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stetho}"
|
||||
debugImplementation "com.squareup.okhttp3:logging-interceptor:${okHttp}"
|
||||
// debugImplementation "com.gu.android:toolargetool:${toolargetool}" // 需要使用调试时才启用
|
||||
debugImplementation "com.github.nichbar:WhatTheStack:${whatTheStack}"
|
||||
|
||||
implementation "androidx.core:core:${core}"
|
||||
implementation "androidx.fragment:fragment:${fragment}"
|
||||
implementation "androidx.core:core-ktx:${core}"
|
||||
implementation "androidx.fragment:fragment-ktx:${fragment}"
|
||||
implementation "androidx.multidex:multidex:${multiDex}"
|
||||
implementation "androidx.appcompat:appcompat:${appCompat}"
|
||||
implementation "androidx.cardview:cardview:${cardView}"
|
||||
implementation "androidx.annotation:annotation:${annotation}"
|
||||
implementation "androidx.constraintlayout:constraintlayout:${constraintLayout}"
|
||||
implementation "androidx.recyclerview:recyclerview:${recyclerView}"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime:${lifeCycle}"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:${lifeCycle}"
|
||||
kapt "androidx.lifecycle:lifecycle-compiler:${lifeCycle}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifeCycle"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifeCycle"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifeCycle"
|
||||
implementation "androidx.room:room-runtime:${room}"
|
||||
implementation "androidx.room:room-rxjava2:${room}"
|
||||
implementation "androidx.core:core-ktx:${ktx}"
|
||||
implementation "androidx.viewpager2:viewpager2:${viewpager2}"
|
||||
kapt "androidx.room:room-compiler:${room}"
|
||||
kapt "androidx.databinding:databinding-compiler:${databinding}"
|
||||
|
||||
implementation "com.google.android.material:material:${material}"
|
||||
|
||||
implementation "com.kyleduo.switchbutton:library:${switchButton}"
|
||||
|
||||
implementation "com.facebook.fresco:fresco:${fresco}"
|
||||
implementation "com.facebook.fresco:animated-gif:${fresco}"
|
||||
implementation "com.facebook.fresco:animated-gif-lite:${fresco}"
|
||||
implementation "com.facebook.fresco:animated-drawable:${fresco}"
|
||||
implementation "com.facebook.fresco:animated-webp:${fresco}"
|
||||
implementation "com.facebook.fresco:webpsupport:${fresco}"
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:${okHttp}"
|
||||
|
||||
@ -236,18 +275,16 @@ 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}"
|
||||
// implementation "com.tencent.bugly:crashreport_upgrade:${buglyTinkerSupport}"
|
||||
|
||||
implementation 'com.google.android:flexbox:1.1.0'
|
||||
implementation "com.google.android:flexbox:${flexbox}"
|
||||
|
||||
implementation "pub.devrel:easypermissions:${easypermissions}"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
implementation 'com.contrarywind:Android-PickerView:4.1.8'
|
||||
implementation "com.contrarywind:Android-PickerView:${pickerView}"
|
||||
|
||||
implementation "com.scwang.smartrefresh:SmartRefreshLayout:${smartRefreshLayout}"
|
||||
implementation "net.cachapa.expandablelayout:expandablelayout:${expandableLayout}"
|
||||
@ -260,50 +297,62 @@ 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.shuyu:gsyVideoPlayer-armv7a:$gsyVideo"
|
||||
// implementation "com.shuyu:gsyVideoPlayer-x86:$gsyVideo"
|
||||
|
||||
implementation "com.github.wendux:DSBridge-Android:$dsBridge"
|
||||
implementation "com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-exo_player2:$gsyVideo"
|
||||
|
||||
implementation "android.arch.work:work-runtime:${workManager}"
|
||||
|
||||
implementation "com.llew.huawei:verifier:1.0.6"
|
||||
implementation "com.llew.huawei:verifier:${verifier}"
|
||||
|
||||
implementation "com.github.tbruyelle:rxpermissions:${rxPermissions}"
|
||||
|
||||
implementation 'com.ethanhua:skeleton:1.1.1'
|
||||
implementation 'io.supercharge:shimmerlayout:2.1.0'
|
||||
implementation "com.tencent.mm.opensdk:wechat-sdk-android-without-mta:5.3.1"
|
||||
implementation 'com.walkud.rom.checker:RomChecker:1.0.0'
|
||||
implementation "com.ethanhua:skeleton:${skeleton}"
|
||||
implementation "io.supercharge:shimmerlayout:${shimmerlayout}"
|
||||
implementation "com.tencent.mm.opensdk:wechat-sdk-android-without-mta:${mta}"
|
||||
implementation "com.github.nichbar:AndroidRomChecker:${romChecker}"
|
||||
|
||||
debugImplementation "com.github.nichbar.chucker:library:$chucker"
|
||||
releaseImplementation "com.github.nichbar.chucker:library-no-op:$chucker"
|
||||
implementation "com.bytedance.applog:RangersAppLog-Lite-cn:$bytedanceApplog"
|
||||
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:2.9.2'
|
||||
implementation "com.aliyun.dpa:oss-android-sdk:${oss}"
|
||||
|
||||
implementation "com.airbnb.android:lottie:$lottie"
|
||||
implementation "com.airbnb.android:lottie:${lottie}"
|
||||
|
||||
implementation("com.github.piasy:BigImageViewer:$bigImageViewer", {
|
||||
implementation "net.lingala.zip4j:zip4j:${zip4j}"
|
||||
|
||||
// plugin 需要字符串,故不能用值
|
||||
implementation "io.sentry:sentry-android:3.2.0"
|
||||
|
||||
implementation("com.github.piasy:BigImageViewer:${bigImageViewer}", {
|
||||
exclude group: 'com.squareup.okhttp3'
|
||||
exclude group: 'androidx.swiperefreshlayout'
|
||||
exclude group: 'com.github.bumptech.glide'
|
||||
exclude group: 'com.facebook.fresco'
|
||||
})
|
||||
implementation "com.github.PhilJay:MPAndroidChart:${chart}"
|
||||
|
||||
debugImplementation "com.github.markzhai:blockcanary-android:$blockcanary"
|
||||
releaseImplementation "com.github.markzhai:blockcanary-no-op:$blockcanary"
|
||||
implementation "com.github.hsiafan:apk-parser:${apkParser}"
|
||||
implementation "org.nanohttpd:nanohttpd:${nanohttpd}"
|
||||
|
||||
implementation "com.aliyun.openservices:aliyun-log-android-sdk:${aliyunLog}"
|
||||
implementation "com.github.princekin-f:EasyFloat:${easyFloat}"
|
||||
|
||||
implementation "io.github.florent37:shapeofview:$shapeOfView"
|
||||
|
||||
implementation 'io.github.sinaweibosdk:core:11.6.0@aar'
|
||||
|
||||
implementation project(':libraries:LGLibrary')
|
||||
implementation project(':libraries:MTA')
|
||||
// implementation project(':libraries:MTA')
|
||||
implementation project(':libraries:QQShare')
|
||||
implementation project(':libraries:TalkingData')
|
||||
implementation project(':libraries:UmengPush')
|
||||
// implementation project(':libraries:TalkingData')
|
||||
// implementation project(':libraries:UmengPush')
|
||||
// implementation project(':libraries:WechatShare')
|
||||
implementation project(':libraries:im')
|
||||
// implementation project(':libraries:im')
|
||||
implementation project(':libraries:Matisse')
|
||||
implementation project(path: ':libraries:gsyVideoPlayer-proxy_cache')
|
||||
}
|
||||
File propFile = file('sign.properties')
|
||||
if (propFile.exists()) {
|
||||
@ -334,7 +383,7 @@ if (propFile.exists()) {
|
||||
android.buildTypes.release.signingConfig = null
|
||||
}
|
||||
|
||||
// 用于测试读取 META-INF 里的 JSON 的代码
|
||||
// 用于测试读取 META-INF 里的文件
|
||||
//task generateMetaJson {
|
||||
// def resDir = new File(buildDir, 'generated/FILES_FOR_META_INF/')
|
||||
// def destDir = new File(resDir, 'META-INF/')
|
||||
@ -361,3 +410,159 @@ if (propFile.exists()) {
|
||||
// task.name.startsWith('merge') && task.name.endsWith('Resources')
|
||||
// }.each { t -> t.dependsOn generateMetaJson }
|
||||
//}
|
||||
|
||||
andResGuard {
|
||||
mappingFile = null
|
||||
use7zip = true
|
||||
useSign = true
|
||||
// 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
|
||||
keepRoot = false
|
||||
// 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
|
||||
fixedResName = "arg"
|
||||
// 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
|
||||
mergeDuplicatedRes = true
|
||||
whiteList = [
|
||||
"R.drawable.icon",
|
||||
"R.drawable.bg_notification_answer_style_1",
|
||||
"R.drawable.bg_notification_answer_style_2",
|
||||
"R.drawable.bg_notification_article_style_1",
|
||||
"R.drawable.bg_notification_article_style_2",
|
||||
"R.drawable.bg_notification_feedback_style_1",
|
||||
"R.drawable.bg_notification_feedback_style_2",
|
||||
"R.drawable.bg_notification_gift_style_1",
|
||||
"R.drawable.bg_notification_gift_style_2",
|
||||
"R.drawable.bg_notification_login_style_1",
|
||||
"R.drawable.bg_notification_login_style_2",
|
||||
"R.drawable.bg_notification_question_style_1",
|
||||
"R.drawable.bg_notification_question_style_2",
|
||||
"R.drawable.bg_notification_rating_style_1",
|
||||
"R.drawable.bg_notification_rating_style_2",
|
||||
"R.drawable.bg_notification_reserve_game_style_1",
|
||||
"R.drawable.bg_notification_reserve_game_style_2",
|
||||
"R.drawable.bg_notification_video_style_1",
|
||||
"R.drawable.bg_notification_video_style_2",
|
||||
"R.drawable.ic_search_no_1",
|
||||
"R.drawable.ic_search_no_2",
|
||||
"R.drawable.ic_search_no_3",
|
||||
"R.drawable.ic_search_no_4",
|
||||
"R.drawable.ic_search_no_5",
|
||||
"R.drawable.ic_search_no_6",
|
||||
"R.drawable.ic_search_no_7",
|
||||
"R.drawable.ic_search_no_8",
|
||||
"R.drawable.ic_search_no_9",
|
||||
"R.drawable.ic_search_no_10",
|
||||
"R.drawable.ic_search_no_11",
|
||||
"R.drawable.ic_search_no_12",
|
||||
"R.drawable.ic_search_no_13",
|
||||
"R.drawable.ic_search_no_14",
|
||||
"R.drawable.ic_search_no_15",
|
||||
"R.drawable.ic_search_no_16",
|
||||
"R.drawable.ic_search_no_17",
|
||||
"R.drawable.ic_search_no_18",
|
||||
"R.drawable.ic_search_no_19",
|
||||
"R.drawable.ic_search_no_20",
|
||||
"R.drawable.ic_recommend_activity",
|
||||
"R.drawable.ic_recommend_discount",
|
||||
"R.drawable.ic_recommend_function",
|
||||
"R.drawable.ic_recommend_gift",
|
||||
"R.drawable.ic_recommend_role",
|
||||
"R.drawable.login_btn_bg",
|
||||
"R.drawable.ic_quick_login_check",
|
||||
"R.drawable.ic_quick_login_uncheck",
|
||||
"R.anim.anim_auth_in",
|
||||
"R.anim.anim_auth_out",
|
||||
]
|
||||
compressFilePattern = [
|
||||
"*.png",
|
||||
"*.jpg",
|
||||
"*.jpeg",
|
||||
"*.gif",
|
||||
]
|
||||
sevenzip {
|
||||
artifact = 'com.tencent.mm:SevenZip:1.2.20'
|
||||
}
|
||||
}
|
||||
|
||||
project.afterEvaluate {
|
||||
def variants = null
|
||||
try {
|
||||
variants = android.applicationVariants
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace()
|
||||
try {
|
||||
variants = android.libraryVariants
|
||||
} catch (Throwable tt) {
|
||||
tt.printStackTrace()
|
||||
}
|
||||
}
|
||||
if (variants != null) {
|
||||
variants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def task = output.processManifestProvider.get()
|
||||
if (task == null) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 Manifest 的 Activity 的 configChanges 添加自己手动处理 configurationChanges 配置 [https://developer.android.com/guide/topics/resources/runtime-changes]
|
||||
* AGP 4.1.0 从 ProcessManifest task 里拿 manifest 的 API 变更调整可以参考这里 [https://github.com/Tencent/tinker/pull/1476/commits/d71645729b13d545ca4ba6826f93fbf558751434]
|
||||
* (搞半天还是不会抽离方法,有空再把 gradle 改成用 kotlin 实现吧)
|
||||
*/
|
||||
task.doLast {
|
||||
def manifestFile = new File(multiApkManifestOutputDirectory.get().asFile, "AndroidManifest.xml")
|
||||
if (manifestFile == null || !manifestFile.exists()) {
|
||||
return
|
||||
}
|
||||
|
||||
String[] configChanges = [
|
||||
"density",
|
||||
"fontScale",
|
||||
"keyboard",
|
||||
"keyboardHidden",
|
||||
"layoutDirection",
|
||||
"locale",
|
||||
"mcc",
|
||||
"mnc",
|
||||
"navigation",
|
||||
"orientation",
|
||||
"screenLayout",
|
||||
"screenSize",
|
||||
"smallestScreenSize",
|
||||
"touchscreen",
|
||||
"uiMode"]
|
||||
|
||||
def parser = new XmlSlurper(false, true)
|
||||
def manifest = parser.parse(manifestFile)
|
||||
def app = manifest.'application'[0]
|
||||
app.'activity'.each { act ->
|
||||
String value = act.attributes()['android:configChanges']
|
||||
if (value == null || value.isEmpty()) {
|
||||
if (value == null) value = ""
|
||||
configChanges.eachWithIndex { config, index ->
|
||||
if (index != configChanges.length - 1) {
|
||||
value += config + "|"
|
||||
} else {
|
||||
value += config
|
||||
}
|
||||
}
|
||||
act.attributes()['androidconfigChanges'] = value
|
||||
} else {
|
||||
String[] valueSplit = value.split("\\|")
|
||||
println configChanges
|
||||
configChanges.eachWithIndex { config, index ->
|
||||
if (!valueSplit.contains(config)) {
|
||||
value += ("|" + config)
|
||||
}
|
||||
}
|
||||
act.attributes()['android:configChanges'] = value
|
||||
}
|
||||
}
|
||||
|
||||
def tmpManifest = XmlUtil.serialize(manifest).replaceAll("androidconfigChanges", "android:configChanges")
|
||||
manifest = parser.parseText(tmpManifest)
|
||||
manifestFile.setText(XmlUtil.serialize(manifest), "utf-8")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
BIN
app/libs/gid-1.3.jar
Normal file
BIN
app/libs/gid-1.3.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/libs/quick_login_android_5.8.1.aar
Normal file
BIN
app/libs/quick_login_android_5.8.1.aar
Normal file
Binary file not shown.
271
app/proguard-rules-legacy.txt
Normal file
271
app/proguard-rules-legacy.txt
Normal file
@ -0,0 +1,271 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\Android\sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
#--------- remove logs start ----------------
|
||||
-assumenosideeffects class com.lightgame.config.CommonDebug {
|
||||
private static String getLogTag(...);
|
||||
private static String getMethodName();
|
||||
public static void logMethodName(...);
|
||||
public static void logParams(...);
|
||||
public static void logFields(...);
|
||||
public static void logMethodWithParams(...);
|
||||
}
|
||||
#-assumenosideeffects class com.lightgame.config.CommonDebug {*;}
|
||||
|
||||
#-dontoptimize
|
||||
#--------- remove logs end ----------------
|
||||
|
||||
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
|
||||
-dontwarn InnerClasses
|
||||
|
||||
# OrmLite uses reflection
|
||||
-keep class com.j256.**
|
||||
-keepclassmembers class com.j256.** { *; }
|
||||
-keep enum com.j256.**
|
||||
-keepclassmembers enum com.j256.** { *; }
|
||||
-keep interface com.j256.**
|
||||
-keepclassmembers interface com.j256.** { *; }
|
||||
-dontwarn com.j256.**
|
||||
|
||||
#okhttp3
|
||||
-dontwarn com.squareup.okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-keep class com.squareup.okhttp3.** { *;}
|
||||
|
||||
# stetho
|
||||
-keep class com.facebook.stetho.** { *; }
|
||||
-dontwarn com.facebook.stetho.**
|
||||
|
||||
# Retrofit 2.2
|
||||
# Platform calls Class.forName on types which do not exist on Android to determine platform.
|
||||
-dontnote retrofit2.Platform
|
||||
# Platform used when running on Java 8 VMs. Will not be used at runtime.
|
||||
-dontwarn retrofit2.Platform$Java8
|
||||
# Retain generic type information for use by reflection by converters and adapters.
|
||||
-keepattributes Signature
|
||||
# Retain declared checked exceptions for use by a Proxy instance.
|
||||
-keepattributes Exceptions
|
||||
|
||||
# Retrofit 2.X
|
||||
## https://square.github.io/retrofit/ ##
|
||||
|
||||
-dontwarn retrofit2.**
|
||||
-keep class retrofit2.** { *; }
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@retrofit2.http.* <methods>;
|
||||
}
|
||||
|
||||
|
||||
# rxjava
|
||||
-keep class rx.schedulers.Schedulers {
|
||||
public static <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.ImmediateScheduler {
|
||||
public <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.TestScheduler {
|
||||
public <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.Schedulers {
|
||||
public static ** test();
|
||||
}
|
||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||
long producerIndex;
|
||||
long consumerIndex;
|
||||
}
|
||||
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
|
||||
long producerNode;
|
||||
long consumerNode;
|
||||
}
|
||||
-dontwarn rx.internal.util.**
|
||||
|
||||
## AutoScrollViewPager
|
||||
-keep class cn.trinea.android.** { *; }
|
||||
-keepclassmembers class cn.trinea.android.** { *; }
|
||||
-dontwarn cn.trinea.android.**
|
||||
|
||||
## butterknife
|
||||
# Retain generated class which implement Unbinder.
|
||||
#-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
|
||||
#
|
||||
## Prevent obfuscation of types which use ButterKnife annotations since the simple name
|
||||
## is used to reflectively look up the generated ViewBinding.
|
||||
#-keep class butterknife.*
|
||||
#-keepclasseswithmembernames class * { @butterknife.* <methods>; }
|
||||
#-keepclasseswithmembernames class * { @butterknife.* <fields>; }
|
||||
|
||||
-dontwarn butterknife.internal.**
|
||||
-keep class **$$ViewInjector { *; }
|
||||
-keepnames class * { @butterknife.InjectView *;}
|
||||
-dontwarn butterknife.Views$InjectViewProcessor
|
||||
-dontwarn com.gc.materialdesign.views.**
|
||||
|
||||
# eventbus
|
||||
-keepattributes *Annotation*
|
||||
-keepclassmembers class ** {
|
||||
@org.greenrobot.eventbus.Subscribe <methods>;
|
||||
}
|
||||
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
|
||||
|
||||
# Only required if you use AsyncExecutor
|
||||
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
|
||||
<init>(java.lang.Throwable);
|
||||
}
|
||||
|
||||
# weiboSdk
|
||||
-keep class com.sina.weibo.sdk.** { *; }
|
||||
-dontwarn android.webkit.WebView
|
||||
-dontwarn android.webkit.WebViewClient
|
||||
|
||||
# app models
|
||||
-keep class com.gh.common.view.** {*;}
|
||||
-keep class com.gh.gamecenter.db.info.** {*;}
|
||||
-keep class com.gh.gamecenter.entity.** {*;}
|
||||
-keep class com.gh.gamecenter.qa.entity.** {*;}
|
||||
-keep class com.gh.gamecenter.retrofit.** {*;}
|
||||
-keep class com.gh.gamecenter.eventbus.** {*;}
|
||||
-keep class com.gh.gamecenter.video.detail.** {*;}
|
||||
-keep class * extends rx.Subscriber
|
||||
|
||||
#---------------------------------webview------------------------------------
|
||||
-keepclassmembers class * extends android.webkit.WebViewClient {
|
||||
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
|
||||
public boolean *(android.webkit.WebView, java.lang.String);
|
||||
}
|
||||
-keepclassmembers class * extends android.webkit.WebViewClient {
|
||||
public void *(android.webkit.WebView, java.lang.String);
|
||||
}
|
||||
#----------------------------------------------------------------------------
|
||||
|
||||
|
||||
##---------------Begin: proguard configuration for Gson ----------
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
#-keep class com.google.gson.stream.** { *; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
-keepclassmembers enum * { *; }
|
||||
|
||||
##---------------End: proguard configuration for Gson ----------
|
||||
|
||||
# ------ bugly ---------
|
||||
-dontwarn com.tencent.bugly.**
|
||||
-keep public class com.tencent.bugly.**{*;}
|
||||
|
||||
# easypermission
|
||||
-keepclassmembers class * {
|
||||
@pub.devrel.easypermissions.AfterPermissionGranted <methods>;
|
||||
}
|
||||
|
||||
# 重命名文件为SourceFile,再配合mapping符号表,可以拿到真实的类名
|
||||
-renamesourcefileattribute SourceFile
|
||||
# 保留源文件行号
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
-ignorewarnings
|
||||
|
||||
-keep @androidx.annotation.Keep class *
|
||||
-keepclassmembers class ** {
|
||||
@androidx.annotation.Keep *;
|
||||
}
|
||||
|
||||
-keep class com.gh.loghub.** { *; }
|
||||
|
||||
### greenDAO 3
|
||||
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
|
||||
public static java.lang.String TABLENAME;
|
||||
}
|
||||
-keep class **$Properties
|
||||
-keep class org.greenrobot.greendao.** { *; }
|
||||
# If you do not use SQLCipher:
|
||||
-dontwarn org.greenrobot.greendao.database.**
|
||||
# If you do not use RxJava:
|
||||
-dontwarn rx.**
|
||||
-dontwarn org.greenrobot.greendao.rx.**
|
||||
-dontwarn org.greenrobot.greendao.**
|
||||
|
||||
### fastJson
|
||||
-dontwarn com.alibaba.fastjson.**
|
||||
-keep class com.alibaba.fastjson.** { *; }
|
||||
-keepattributes Signature
|
||||
-keepattributes Annotation
|
||||
|
||||
### 广点通
|
||||
-dontwarn com.qq.gdt.action.**
|
||||
-keep class com.qq.gdt.action.** {*;}
|
||||
|
||||
### AndroidX
|
||||
-keep class androidx.core.app.CoreComponentFactory { *; }
|
||||
|
||||
#阿里云上传
|
||||
-keep class com.alibaba.sdk.android.oss.** { *; }
|
||||
-dontwarn okio.**
|
||||
-dontwarn org.apache.commons.codec.binary.**
|
||||
|
||||
#视频相关
|
||||
-keep class com.shuyu.gsyvideoplayer.video.** { *; }
|
||||
-dontwarn com.shuyu.gsyvideoplayer.video.**
|
||||
-keep class com.shuyu.gsyvideoplayer.video.base.** { *; }
|
||||
-dontwarn com.shuyu.gsyvideoplayer.video.base.**
|
||||
-keep class com.shuyu.gsyvideoplayer.utils.** { *; }
|
||||
-dontwarn com.shuyu.gsyvideoplayer.utils.**
|
||||
-keep class tv.danmaku.ijk.** { *; }
|
||||
-dontwarn tv.danmaku.ijk.**
|
||||
-keep public class * extends android.view.View{
|
||||
*** get*();
|
||||
void set*(***);
|
||||
public <init>(android.content.Context);
|
||||
public <init>(android.content.Context, android.util.AttributeSet);
|
||||
public <init>(android.content.Context, android.util.AttributeSet, int);
|
||||
}
|
||||
|
||||
#穿山甲
|
||||
-keep class com.bytedance.sdk.openadsdk.** { *; }
|
||||
-keep public interface com.bytedance.sdk.openadsdk.downloadnew.** {*;}
|
||||
-keep class com.pgl.sys.ces.* {*;}
|
||||
|
||||
-keep class com.gyf.immersionbar.* {*;}
|
||||
-dontwarn com.gyf.immersionbar.**
|
||||
|
||||
-keep class com.taobao.securityjni.**{*;}
|
||||
-keep class com.taobao.wireless.security.**{*;}
|
||||
-keep class com.ut.secbody.**{*;}
|
||||
-keep class com.taobao.dp.**{*;}
|
||||
-keep class com.alibaba.wireless.security.**{*;}
|
||||
|
||||
-keep class com.alibaba.sdk.android.**{*;}
|
||||
-keep class com.ut.**{*;}
|
||||
-keep class com.ta.**{*;}
|
||||
|
||||
-keep class com.gh.gamecenter.GdtHelper { *; }
|
||||
-keep class com.gh.gamecenter.TeaHelper { *; }
|
||||
@ -1,20 +1,3 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\Android\sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
#--------- remove logs start ----------------
|
||||
-assumenosideeffects class com.lightgame.config.CommonDebug {
|
||||
@ -26,126 +9,66 @@
|
||||
public static void logMethodWithParams(...);
|
||||
}
|
||||
|
||||
#-assumenosideeffects class com.lightgame.config.CommonDebug {*;}
|
||||
|
||||
#-dontoptimize
|
||||
-assumenosideeffects class com.lightgame.utils.Utils {
|
||||
public static void log(...);
|
||||
}
|
||||
#--------- remove logs end ----------------
|
||||
|
||||
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
|
||||
-dontwarn InnerClasses
|
||||
# TODO Dicard sourceFile in final release build but remain in internal build.
|
||||
-renamesourcefileattribute SourceFile
|
||||
# Keep Attribute
|
||||
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod,SourceFile,LineNumberTable
|
||||
|
||||
# OrmLite uses reflection
|
||||
-keep class com.j256.**
|
||||
-keepclassmembers class com.j256.** { *; }
|
||||
-keep enum com.j256.**
|
||||
-keepclassmembers enum com.j256.** { *; }
|
||||
-keep interface com.j256.**
|
||||
-keepclassmembers interface com.j256.** { *; }
|
||||
# OrmLite
|
||||
-keep class com.j256.*
|
||||
-keepclassmembers class com.j256.* { *; }
|
||||
-keep enum com.j256.*
|
||||
-keepclassmembers enum com.j256.* { *; }
|
||||
-keep interface com.j256.*
|
||||
-keepclassmembers interface com.j256.* { *; }
|
||||
-dontwarn com.j256.**
|
||||
|
||||
#okhttp3
|
||||
-dontwarn com.squareup.okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-keep class com.squareup.okhttp3.** { *;}
|
||||
|
||||
# stetho
|
||||
-keep class com.facebook.stetho.** { *; }
|
||||
-dontwarn com.facebook.stetho.**
|
||||
|
||||
# Retrofit 2.2
|
||||
# Platform calls Class.forName on types which do not exist on Android to determine platform.
|
||||
-dontnote retrofit2.Platform
|
||||
# Platform used when running on Java 8 VMs. Will not be used at runtime.
|
||||
-dontwarn retrofit2.Platform$Java8
|
||||
# Retain generic type information for use by reflection by converters and adapters.
|
||||
-keepattributes Signature
|
||||
# Retain declared checked exceptions for use by a Proxy instance.
|
||||
-keepattributes Exceptions
|
||||
|
||||
# Retrofit 2.X
|
||||
## https://square.github.io/retrofit/ ##
|
||||
|
||||
-dontwarn retrofit2.**
|
||||
-keep class retrofit2.** { *; }
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@retrofit2.http.* <methods>;
|
||||
}
|
||||
|
||||
|
||||
# rxjava
|
||||
-keep class rx.schedulers.Schedulers {
|
||||
public static <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.ImmediateScheduler {
|
||||
public <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.TestScheduler {
|
||||
public <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.Schedulers {
|
||||
public static ** test();
|
||||
}
|
||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||
long producerIndex;
|
||||
long consumerIndex;
|
||||
}
|
||||
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
|
||||
long producerNode;
|
||||
long consumerNode;
|
||||
}
|
||||
-dontwarn rx.internal.util.**
|
||||
|
||||
## AutoScrollViewPager
|
||||
-keep class cn.trinea.android.** { *; }
|
||||
-keepclassmembers class cn.trinea.android.** { *; }
|
||||
### AutoScrollViewPager
|
||||
-keep class cn.trinea.android.* { *; }
|
||||
-keepclassmembers class cn.trinea.android.* { *; }
|
||||
-dontwarn cn.trinea.android.**
|
||||
|
||||
## butterknife
|
||||
# Retain generated class which implement Unbinder.
|
||||
#-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
|
||||
#
|
||||
## Prevent obfuscation of types which use ButterKnife annotations since the simple name
|
||||
## is used to reflectively look up the generated ViewBinding.
|
||||
#-keep class butterknife.*
|
||||
#-keepclasseswithmembernames class * { @butterknife.* <methods>; }
|
||||
#-keepclasseswithmembernames class * { @butterknife.* <fields>; }
|
||||
### Butterknife
|
||||
-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
|
||||
-keep class butterknife.*
|
||||
-keepclasseswithmembernames class * { @butterknife.* <methods>; }
|
||||
-keepclasseswithmembernames class * { @butterknife.* <fields>; }
|
||||
|
||||
-dontwarn butterknife.internal.**
|
||||
-keep class **$$ViewInjector { *; }
|
||||
-keepnames class * { @butterknife.InjectView *;}
|
||||
-dontwarn butterknife.Views$InjectViewProcessor
|
||||
-dontwarn com.gc.materialdesign.views.**
|
||||
|
||||
# eventbus
|
||||
-keepattributes *Annotation*
|
||||
-keepclassmembers class ** {
|
||||
### eventbus
|
||||
-keepclassmembers class * {
|
||||
@org.greenrobot.eventbus.Subscribe <methods>;
|
||||
}
|
||||
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
|
||||
|
||||
# Only required if you use AsyncExecutor
|
||||
### Only required if you use AsyncExecutor
|
||||
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
|
||||
<init>(java.lang.Throwable);
|
||||
}
|
||||
|
||||
# weiboSdk
|
||||
### weiboSdk
|
||||
-keep class com.sina.weibo.sdk.** { *; }
|
||||
-dontwarn android.webkit.WebView
|
||||
-dontwarn android.webkit.WebViewClient
|
||||
|
||||
# app models
|
||||
-keep class com.gh.common.view.** {*;}
|
||||
-keep class com.gh.gamecenter.db.info.** {*;}
|
||||
-keep class com.gh.gamecenter.entity.** {*;}
|
||||
-keep class com.gh.gamecenter.qa.entity.** {*;}
|
||||
-keep class com.gh.gamecenter.retrofit.** {*;}
|
||||
-keep class com.gh.gamecenter.eventbus.** {*;}
|
||||
-keep class * extends rx.Subscriber
|
||||
### wechatSdk
|
||||
### TODO 这里用 com.tencent.*{*;} 不起效?但其它地方可以?
|
||||
-keep class com.tencent.**{*;}
|
||||
|
||||
#---------------------------------webview------------------------------------
|
||||
### app models
|
||||
-keep class com.gh.common.view.* {*;}
|
||||
-keep class com.gh.gamecenter.db.info.* {*;}
|
||||
-keep class com.gh.gamecenter.entity.* {*;}
|
||||
-keep class com.gh.gamecenter.qa.entity.* {*;}
|
||||
-keep class com.gh.gamecenter.retrofit.* {*;}
|
||||
-keep class com.gh.gamecenter.eventbus.* {*;}
|
||||
-keep class com.gh.gamecenter.video.detail.* {*;}
|
||||
|
||||
###
|
||||
-keepclassmembers class * extends android.webkit.WebViewClient {
|
||||
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
|
||||
public boolean *(android.webkit.WebView, java.lang.String);
|
||||
@ -153,91 +76,67 @@
|
||||
-keepclassmembers class * extends android.webkit.WebViewClient {
|
||||
public void *(android.webkit.WebView, java.lang.String);
|
||||
}
|
||||
#----------------------------------------------------------------------------
|
||||
|
||||
|
||||
##---------------Begin: proguard configuration for Gson ----------
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
#-keep class com.google.gson.stream.** { *; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
-keepclassmembers enum * { *; }
|
||||
|
||||
##---------------End: proguard configuration for Gson ----------
|
||||
|
||||
# ------ bugly ---------
|
||||
-dontwarn com.tencent.bugly.**
|
||||
-keep public class com.tencent.bugly.**{*;}
|
||||
|
||||
# easypermission
|
||||
### easypermission
|
||||
-keepclassmembers class * {
|
||||
@pub.devrel.easypermissions.AfterPermissionGranted <methods>;
|
||||
}
|
||||
|
||||
# 重命名文件为SourceFile,再配合mapping符号表,可以拿到真实的类名
|
||||
-renamesourcefileattribute SourceFile
|
||||
# 保留源文件行号
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# TODO What's this ?
|
||||
-ignorewarnings
|
||||
|
||||
### Keep Annotation
|
||||
-keep @androidx.annotation.Keep class *
|
||||
-keepclassmembers class ** {
|
||||
-keepclassmembers class * {
|
||||
@androidx.annotation.Keep *;
|
||||
}
|
||||
|
||||
-keep class com.gh.loghub.** { *; }
|
||||
|
||||
### greenDAO 3
|
||||
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
|
||||
public static java.lang.String TABLENAME;
|
||||
}
|
||||
-keep class **$Properties
|
||||
-keep class org.greenrobot.greendao.** { *; }
|
||||
# If you do not use SQLCipher:
|
||||
-dontwarn org.greenrobot.greendao.database.**
|
||||
# If you do not use RxJava:
|
||||
-dontwarn rx.**
|
||||
-dontwarn org.greenrobot.greendao.rx.**
|
||||
-dontwarn org.greenrobot.greendao.**
|
||||
|
||||
### fastJson
|
||||
-dontwarn com.alibaba.fastjson.**
|
||||
-keep class com.alibaba.fastjson.** { *; }
|
||||
-keepattributes Signature
|
||||
-keepattributes Annotation
|
||||
|
||||
### 广点通
|
||||
-dontwarn com.qq.gdt.action.**
|
||||
-keep class com.qq.gdt.action.** {*;}
|
||||
-keep class com.qq.gdt.action.* {*;}
|
||||
|
||||
### AndroidX
|
||||
-keep class androidx.core.app.CoreComponentFactory { *; }
|
||||
|
||||
#阿里云上传
|
||||
-keep class com.alibaba.sdk.android.oss.** { *; }
|
||||
### 阿里云上传
|
||||
-keep class com.alibaba.sdk.android.oss.* { *; }
|
||||
-dontwarn okio.**
|
||||
-dontwarn org.apache.commons.codec.binary.**
|
||||
|
||||
#视频相关
|
||||
-keep class com.shuyu.gsyvideoplayer.video.** { *; }
|
||||
### 视频相关
|
||||
-keep class com.shuyu.gsyvideoplayer.video.* { *; }
|
||||
-dontwarn com.shuyu.gsyvideoplayer.video.**
|
||||
-keep class com.shuyu.gsyvideoplayer.video.base.** { *; }
|
||||
-keep class com.shuyu.gsyvideoplayer.video.base.* { *; }
|
||||
-dontwarn com.shuyu.gsyvideoplayer.video.base.**
|
||||
-keep class com.shuyu.gsyvideoplayer.utils.** { *; }
|
||||
-keep class com.shuyu.gsyvideoplayer.utils.* { *; }
|
||||
-dontwarn com.shuyu.gsyvideoplayer.utils.**
|
||||
-keep class tv.danmaku.ijk.** { *; }
|
||||
-dontwarn tv.danmaku.ijk.**
|
||||
-keep class tv.danmaku.ijk.* { *; }
|
||||
-dontwarn tv.danmaku.ijk.**
|
||||
-keep public class * extends android.view.View{
|
||||
*** get*();
|
||||
void set*(***);
|
||||
public <init>(android.content.Context);
|
||||
public <init>(android.content.Context, android.util.AttributeSet);
|
||||
public <init>(android.content.Context, android.util.AttributeSet, int);
|
||||
}
|
||||
|
||||
-keep class com.alibaba.sdk.android.*{*;}
|
||||
-keep class com.ut.*{*;}
|
||||
-keep class com.ta.*{*;}
|
||||
|
||||
### GDT & TEA
|
||||
-keep class com.gh.gamecenter.GdtHelper { *; }
|
||||
-keep class com.gh.gamecenter.TeaHelper { *; }
|
||||
|
||||
### 阿里云日志
|
||||
-keep class com.aliyun.sls.android.producer.* { *; }
|
||||
-keep interface com.aliyun.sls.android.producer.* { *; }
|
||||
|
||||
### 中国移动一键登录
|
||||
-dontwarn com.cmic.sso.sdk.**
|
||||
-keep class com.cmic.sso.sdk.* { *; }
|
||||
|
||||
### EasyFloat
|
||||
-keep class com.lzf.easyfloat.* {*;}
|
||||
|
||||
### 避免 WebChromeClient 被混淆
|
||||
-keepclassmembers class * extends android.webkit.WebChromeClient{
|
||||
public void openFileChooser(...);
|
||||
}
|
||||
@ -2,9 +2,6 @@ package com.gh.gamecenter;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.facebook.stetho.Stetho;
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor;
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
@ -18,18 +15,8 @@ import okhttp3.logging.HttpLoggingInterceptor;
|
||||
public class Injection {
|
||||
|
||||
public static boolean appInit(Application application) {
|
||||
|
||||
// init leakcanary
|
||||
if (LeakCanary.isInAnalyzerProcess(application)) {
|
||||
// This process is dedicated to LeakCanary for heap analysis.
|
||||
// You should not init your app in this process.
|
||||
return false;
|
||||
}
|
||||
LeakCanary.install(application);
|
||||
|
||||
// init stetho
|
||||
Stetho.initializeWithDefaults(application);
|
||||
|
||||
// 监控Bundle大小,预防溢出(需要调试的时候再开启吧!)
|
||||
// TooLargeTool.startLogging(application);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -38,7 +25,6 @@ public class Injection {
|
||||
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
|
||||
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
builder.addNetworkInterceptor(interceptor);
|
||||
builder.addNetworkInterceptor(new StethoInterceptor());
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
77
app/src/gdt/java/com/gh/gamecenter/GdtHelper.kt
Normal file
77
app/src/gdt/java/com/gh/gamecenter/GdtHelper.kt
Normal file
@ -0,0 +1,77 @@
|
||||
package com.gh.gamecenter
|
||||
|
||||
import android.app.Application
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.gh.common.util.ToastUtils
|
||||
import com.lightgame.utils.Utils
|
||||
import com.qq.gdt.action.GDTAction
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* 广点通辅助类 [https://gitlab.ghzhushou.com/pm/halo-app-issues/issues/403]
|
||||
*
|
||||
* 更换帐号 [https://gitlab.ghzs.com/pm/yunying/issues/893]
|
||||
*/
|
||||
object GdtHelper {
|
||||
|
||||
const val NETWORK_TYPE = "NETWORK_TYPE"
|
||||
const val PAGE_TYPE = "PAGE_TYPE"
|
||||
const val CONTENT_TYPE = "CONTENT_TYPE"
|
||||
const val CONTENT_ID = "CONTENT_ID"
|
||||
const val KEYWORD = "KEYWORD"
|
||||
const val GAME_ID = "GAME_ID"
|
||||
const val SCORE = "SCORE"
|
||||
const val PLATFORM = "PLATFORM"
|
||||
|
||||
@JvmStatic
|
||||
fun init(application: Application, channel: String) {
|
||||
if (shouldUseGdtHelper()) {
|
||||
if (channel == "GH_728") {
|
||||
GDTAction.init(application, "1111012969", "9d3d9da5b0948a317c03d08f14d445dc")
|
||||
} else if (channel == "GH_729") {
|
||||
GDTAction.init(application, "1111013063", "f53dabf458a356b101d99fc4069eb7f1")
|
||||
} else if (channel == "GH_765") {
|
||||
GDTAction.init(application, "1111327925", "588d503f0990f98f9b2394fbb795c570")
|
||||
} else {
|
||||
GDTAction.init(application, "1110680399", "f5ddaafbf520d7d7385499232a408d0a")
|
||||
}
|
||||
}
|
||||
Utils.log("init GdtHelper")
|
||||
}
|
||||
|
||||
// fun logAction(type: String) {
|
||||
// if (shouldUseGdtHelper()) {
|
||||
// GDTAction.logAction(type)
|
||||
// Utils.log("GDT", type)
|
||||
// }
|
||||
// }
|
||||
@JvmStatic
|
||||
fun logAction(type: String, vararg kv: String?) {
|
||||
try {
|
||||
val actionParam = JSONObject()
|
||||
for (i in kv.indices) {
|
||||
if (i % 2 != 0) {
|
||||
val key = kv[i - 1]
|
||||
val value = kv[i]
|
||||
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) {
|
||||
actionParam.put(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
Utils.log("GDT", "$type + [${kv.joinToString(" , ")}]")
|
||||
GDTAction.logAction(type, actionParam)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO 确认开启的渠道条件
|
||||
private fun shouldUseGdtHelper(): Boolean {
|
||||
return true
|
||||
//
|
||||
// val channel = HaloApp.getInstance().channel
|
||||
// return !(TextUtils.isEmpty(channel) || channel.contains("GDT".toLowerCase(Locale.CHINA)))
|
||||
}
|
||||
|
||||
}
|
||||
BIN
app/src/gdt/libs/GDTActionSDK.min.1.6.10.aar
Normal file
BIN
app/src/gdt/libs/GDTActionSDK.min.1.6.10.aar
Normal file
Binary file not shown.
@ -9,8 +9,6 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<!-- 允许应用程序读取扩展存储器 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<!-- 允许挂载和反挂载文件系统可移动存储 -->
|
||||
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
|
||||
<!-- 允许应用程序访问Wi-Fi网络状态信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<!-- 允许应用程序获取网络信息状态 -->
|
||||
@ -23,28 +21,31 @@
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<!-- 允许应用程序改变Wi-Fi连接状态 -->
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<!-- 允许应用程序打开系统窗口,显示其他应用程序 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<!-- 修改系统设置的权限 -->
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
<!-- 允许应用程序改变网络连接状态 -->
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<!-- 允许应用程序快捷方式 -->
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.PACKAGE_USAGE_STATS"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<!-- bugly with tinker -->
|
||||
<uses-permission android:name="android.permission.READ_LOGS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<!-- 如果有视频相关的广告且使用textureView播放,请务必添加,否则黑屏 -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.shuyu.gsyvideoplayer,
|
||||
com.shuyu.gsyvideoplayer.lib,
|
||||
com.haroldadmin.whatthestack,
|
||||
com.shuyu.gsyvideoplayer.armv7a,
|
||||
com.shuyu.gsyvideoplayer.x86,
|
||||
com.shuyu.gsy.base,
|
||||
shuyu.com.androidvideocache,
|
||||
com.google.android.exoplayer2,
|
||||
tv.danmaku.ijk.media.exo2,
|
||||
shuyu.com.androidvideocache,
|
||||
pl.droidsonroids.gif" />
|
||||
pl.droidsonroids.gif,
|
||||
com.lzf.easyfloat"/>
|
||||
|
||||
<!-- 去掉 SDK 一些流氓权限 -->
|
||||
<uses-permission
|
||||
@ -60,16 +61,26 @@
|
||||
|
||||
<!--android:largeHeap = "true"-->
|
||||
<application
|
||||
android:name="com.halo.assistant.TinkerApp"
|
||||
android:name="com.halo.assistant.HaloApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/logo"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/AppCompatTheme.APP"
|
||||
tools:replace="android:allowBackup"
|
||||
tools:targetApi="n">
|
||||
|
||||
<!--android:launchMode = "singleTask"-->
|
||||
<meta-data
|
||||
android:name="io.sentry.auto-init"
|
||||
android:value="false" />
|
||||
|
||||
<!-- 不让 sentry 读取系统事件 -->
|
||||
<meta-data
|
||||
android:name="io.sentry.breadcrumbs.system-events"
|
||||
android:value="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SplashScreenActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
@ -96,8 +107,9 @@
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<!--android:theme = "@android:style/Theme.Black.NoTitleBar.Fullscreen" 退出时屏幕抖动 -->
|
||||
<activity android:name="com.gh.gamecenter.ViewImageActivity" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.ImageViewerActivity"
|
||||
android:theme="@style/Theme.Transparent" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SearchActivity"
|
||||
@ -115,6 +127,10 @@
|
||||
android:name="com.gh.gamecenter.ShellActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.gamedetail.history.HistoryApkListActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.NewsDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
@ -148,6 +164,14 @@
|
||||
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" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.ShareCardPicActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
@ -158,7 +182,8 @@
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.MessageDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/TransparentStatusBarAndNavigationBar" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.LibaoActivity"
|
||||
@ -186,9 +211,18 @@
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.CommentDetailActivity"
|
||||
android:name="com.gh.gamecenter.security.SecurityActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.security.BindPhoneActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.CommentDetailActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/TransparentStatusBarAndNavigationBar" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.mygame.MyGameActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
@ -367,6 +401,10 @@
|
||||
android:name="com.gh.gamecenter.qa.article.detail.ArticleDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.qa.article.detail.comment.ArticleDetailCommentActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.qa.draft.CommunityDraftWrapperActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
@ -398,7 +436,8 @@
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.gamedetail.rating.RatingReplyActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/TransparentStatusBarAndNavigationBar" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.history.HistoryActivity"
|
||||
@ -426,14 +465,15 @@
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.video.upload.view.UploadVideoActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.video.game.GameVideoActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.qa.editor.VideoActivity"
|
||||
android:name="com.gh.gamecenter.qa.editor.LocalMediaActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
@ -458,8 +498,8 @@
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.HelpAndFeedbackActivity"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:screenOrientation="portrait" />
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.help.HelpDetailActivity"
|
||||
@ -471,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"
|
||||
@ -480,11 +526,181 @@
|
||||
android:name=".gamedetail.myrating.MyRatingActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<!-- 使用小米/华为推送弹窗功能提高推送成功率-->
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.PushProxyActivity"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.Translucent" />
|
||||
android:name="com.gh.gamecenter.gamedetail.fuli.kaifu.ServersCalendarActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.QaActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".qa.answer.draft.AnswerDraftActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".gamedetail.rating.RatingFoldActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".video.data.VideoDataActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".video.poster.PosterEditActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".video.poster.PosterClipActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".forum.select.ForumSelectActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".forum.detail.ForumDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
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" />
|
||||
|
||||
<activity
|
||||
android:name=".personalhome.border.AvatarBorderActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".personalhome.background.PersonalityBackgroundActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".personalhome.background.BackgroundPreviewActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".personalhome.background.BackgroundClipActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/TransparentStatusBarAndNavigationBar" />
|
||||
|
||||
<activity
|
||||
android:name=".simulatorgame.SimulatorGameActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".simulatorgame.SimulatorManagementActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".catalog.CatalogActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".catalog.NewCatalogListActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".forum.search.ForumOrUserSearchActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".energy.EnergyCenterActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".energy.EnergyHouseActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".personal.NewPersonalActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".qa.questions.draft.QuestionDraftActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".servers.GameServerTestActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".category2.CategoryV2Activity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".personal.DeliveryInfoActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".qa.editor.PreviewVideoActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".qa.video.publish.VideoPublishActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".setting.GameDownloadSettingActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".setting.VideoSettingActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".qa.video.detail.ForumVideoDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".video.videomanager.VideoDraftActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".qa.questions.newdetail.NewQuestionDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".qa.editor.FullScreenVideoActivity"
|
||||
android:screenOrientation="landscape"
|
||||
android:theme="@style/AppFullScreenTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".forum.list.ForumListActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".qa.answer.detail.SimpleAnswerDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".game.commoncollection.detail.CommonCollectionDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.cmic.sso.sdk.activity.LoginAuthActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@android:style/Theme.Dialog" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".home.skip.PackageSkipActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<!-- <!– 使用小米/华为推送弹窗功能提高推送成功率–>-->
|
||||
<!-- <activity-->
|
||||
<!-- android:name="com.gh.gamecenter.PushProxyActivity"-->
|
||||
<!-- android:exported="true"-->
|
||||
<!-- android:launchMode="singleTask"-->
|
||||
<!-- android:theme="@android:style/Theme.Translucent" />-->
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SkipActivity"
|
||||
@ -496,6 +712,14 @@
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<data android:scheme="market" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@ -506,6 +730,12 @@
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"></activity>
|
||||
|
||||
<!-- <activity-->
|
||||
<!-- android:name="${applicationId}.douyinapi.DouYinEntryActivity"-->
|
||||
<!-- android:launchMode="singleTask"-->
|
||||
<!-- android:taskAffinity="${applicationId}"-->
|
||||
<!-- android:exported="true" />-->
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}"
|
||||
@ -516,6 +746,11 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
tools:node="remove" />
|
||||
|
||||
<receiver
|
||||
android:name="com.gh.gamecenter.receiver.DownloadReceiver"
|
||||
android:exported="false">
|
||||
@ -539,42 +774,81 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name="com.gh.gamecenter.receiver.UmengMessageReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="com.gh.gamecenter.UMENG" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- <receiver android:name="com.gh.gamecenter.receiver.UmengMessageReceiver">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="com.gh.gamecenter.UMENG" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </receiver>-->
|
||||
|
||||
<!--魅族push应用定义消息receiver声明 -->
|
||||
<receiver android:name="com.gh.gamecenter.receiver.MeizuPushReceiver">
|
||||
<intent-filter>
|
||||
<!-- 接收push消息 -->
|
||||
<action android:name="com.meizu.flyme.push.intent.MESSAGE" />
|
||||
<!-- 接收register消息 -->
|
||||
<action android:name="com.meizu.flyme.push.intent.REGISTER.FEEDBACK" />
|
||||
<!-- 接收unregister消息-->
|
||||
<action android:name="com.meizu.flyme.push.intent.UNREGISTER.FEEDBACK" />
|
||||
<!-- 兼容低版本Flyme3推送服务配置 -->
|
||||
<action android:name="com.meizu.c2dm.intent.REGISTRATION" />
|
||||
<action android:name="com.meizu.c2dm.intent.RECEIVE" />
|
||||
<!-- <!–魅族push应用定义消息receiver声明 –>-->
|
||||
<!-- <receiver android:name="com.gh.gamecenter.receiver.UmengMeizuPushReceiver">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <!– 接收push消息 –>-->
|
||||
<!-- <action android:name="com.meizu.flyme.push.intent.MESSAGE" />-->
|
||||
<!-- <!– 接收register消息 –>-->
|
||||
<!-- <action android:name="com.meizu.flyme.push.intent.REGISTER.FEEDBACK" />-->
|
||||
<!-- <!– 接收unregister消息–>-->
|
||||
<!-- <action android:name="com.meizu.flyme.push.intent.UNREGISTER.FEEDBACK" />-->
|
||||
<!-- <!– 兼容低版本Flyme3推送服务配置 –>-->
|
||||
<!-- <action android:name="com.meizu.c2dm.intent.REGISTRATION" />-->
|
||||
<!-- <action android:name="com.meizu.c2dm.intent.RECEIVE" />-->
|
||||
|
||||
<category android:name="${applicationId}" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- <category android:name="${applicationId}" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </receiver>-->
|
||||
|
||||
<receiver
|
||||
android:name="com.gh.common.im.ImReceiver"
|
||||
android:enabled="true">
|
||||
<intent-filter android:priority="2147483647">
|
||||
<action android:name="com.gh.im" />
|
||||
<action android:name="action_finish" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- <receiver-->
|
||||
<!-- android:name="com.gh.common.im.ImReceiver"-->
|
||||
<!-- android:enabled="true">-->
|
||||
<!-- <intent-filter android:priority="2147483647">-->
|
||||
<!-- <action android:name="com.gh.im" />-->
|
||||
<!-- <action android:name="action_finish" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </receiver>-->
|
||||
|
||||
<service android:name="com.gh.base.GHUmengNotificationService" />
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="com.huawei.hms.client.appid"-->
|
||||
<!-- android:value="@string/huawei_push_appid" />-->
|
||||
|
||||
<!-- <service-->
|
||||
<!-- android:name="com.gh.base.GHUmengNotificationService"-->
|
||||
<!-- android:permission="android.permission.BIND_JOB_SERVICE" />-->
|
||||
|
||||
<!--<service android:name = "com.gh.gamecenter.statistics.AppStaticService" />-->
|
||||
|
||||
<!-- 梦工厂配置 开始 -->
|
||||
<!--<meta-data
|
||||
android:name="MGC_APPID"
|
||||
android:value="1001276" />
|
||||
|
||||
<provider
|
||||
android:name="com.leto.game.base.provider.LetoFileProvider"
|
||||
android:authorities="${applicationId}.leto.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/leto_file_path"
|
||||
tools:replace="android:resource" />
|
||||
</provider>-->
|
||||
<!-- 梦工厂配置 结束 -->
|
||||
|
||||
<!-- 穿山甲配置 开始 -->
|
||||
<!--<provider
|
||||
android:name="com.bytedance.sdk.openadsdk.TTFileProvider"
|
||||
android:authorities="${applicationId}.TTFileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="com.bytedance.sdk.openadsdk.multipro.TTMultiProvider"
|
||||
android:authorities="${applicationId}.TTMultiProvider"
|
||||
android:exported="false" />-->
|
||||
<!-- 穿山甲配置 结束 -->
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@ -6,13 +6,16 @@
|
||||
<link rel="stylesheet" type="text/css" href="normalize.css">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link rel="stylesheet" type="text/css" href="video-js.min.css">
|
||||
<!-- <link rel="stylesheet" href="https://static-web.ghzs.com/website-static/lib/video-js.min.css">--> <!--在web页面播放视频-->
|
||||
<!--<link rel="stylesheet" type="text/css" href="https://resource.ghzs.com/css/halo_app.css">-->
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<body style="overflow-x: hidden; word-break: break-all;">
|
||||
<div id="editor" contenteditable="false"></div>
|
||||
<script type="text/javascript" src="zepto.min.js"></script>
|
||||
<script type="text/javascript" src="rich_editor.js"></script>
|
||||
<script type="text/javascript" src="video.min.js"></script>
|
||||
<!--<script src="https://static-web.ghzs.com/website-static/lib/video.min.js"></script>--> <!--在web页面播放视频-->
|
||||
<!--<script type="text/javascript" src="content.js"></script>-->
|
||||
<!--<script type="text/javascript" src="https://resource.ghzs.com/js/halo_app.js"></script>-->
|
||||
</body>
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
emoji_kf_1.png,:smile:
|
||||
emoji_kf_2.png,:smiley:
|
||||
emoji_kf_3.png,:laughing:
|
||||
emoji_kf_4.png,:blush:
|
||||
emoji_kf_5.png,:heart_eyes:
|
||||
emoji_kf_6.png,:smirk:
|
||||
emoji_kf_7.png,:flushed:
|
||||
emoji_kf_8.png,:kissing_heart:
|
||||
emoji_kf_9.png,:grin:
|
||||
emoji_kf_10.png,:wink:
|
||||
emoji_kf_11.png,:stuck_out_tongue_winking_eye:
|
||||
emoji_kf_12.png,:stuck_out_tongue_closed eyes:
|
||||
emoji_kf_13.png,:worried:
|
||||
emoji_kf_14.png,:sleeping:
|
||||
emoji_kf_15.png,:expressionless:
|
||||
emoji_kf_16.png,:sweat_smile:
|
||||
emoji_kf_17.png,:joy:
|
||||
emoji_kf_18.png,:cold_sweat:
|
||||
emoji_kf_19.png,:sob:
|
||||
emoji_kf_20.png,:angry:
|
||||
emoji_kf_21.png,:mask:
|
||||
emoji_kf_22.png,:scream:
|
||||
emoji_kf_23.png,:sunglasses:
|
||||
emoji_kf_24.png,:heart:
|
||||
emoji_kf_25.png,:broken_heart:
|
||||
emoji_kf_26.png,:star:
|
||||
emoji_kf_27.png,:anger:
|
||||
emoji_kf_28.png,:exclamation:
|
||||
emoji_kf_29.png,:question:
|
||||
emoji_kf_30.png,:zzz:
|
||||
emoji_kf_31.png,:thumbsup:
|
||||
emoji_kf_32.png,:thumbsdown:
|
||||
emoji_kf_33.png,:ok_hand:
|
||||
emoji_kf_34.png,:punch:
|
||||
emoji_kf_35.png,:yeah:
|
||||
emoji_kf_36.png,:clap:
|
||||
emoji_kf_37.png,:muscle:
|
||||
emoji_kf_38.png,:pray:
|
||||
emoji_kf_39.png,:skull:
|
||||
emoji_kf_40.png,:trollface:
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
1
app/src/main/assets/lottie/follow.json
Normal file
1
app/src/main/assets/lottie/follow.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/src/main/assets/lottie/tab_forum.json
Normal file
1
app/src/main/assets/lottie/tab_forum.json
Normal file
@ -0,0 +1 @@
|
||||
{"v":"5.6.9","fr":30,"ip":0,"op":20,"w":66,"h":66,"nm":"bottom bar tab/论坛/选中/E","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"白-修正","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[33,33,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":4,"s":[80,80,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":9,"s":[110,110,100]},{"t":13,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[0,-0.66],[0,0],[1.38,0],[0,1.38],[0,0],[-0.66,0],[0,0]],"o":[[0,0],[0,1.38],[-1.38,0],[0,0],[0,-0.66],[0,0],[0.66,0]],"v":[[2.5,-1.3],[2.5,0],[0,2.5],[-2.5,0],[-2.5,-1.3],[-1.3,-2.5],[1.3,-2.5]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[0,-0.66],[0,0],[1.38,0],[0,1.38],[0,0],[-0.66,0],[0,0]],"o":[[0,0],[0,1.38],[-1.38,0],[0,0],[0,-0.66],[0,0],[0.66,0]],"v":[[2.5,-0.102],[2.5,0],[0,2.109],[-2.5,0],[-2.5,-0.102],[-1.3,-1.302],[1.3,-1.302]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":9,"s":[{"i":[[0,-0.66],[0,0],[1.38,0],[0,1.38],[0,0],[-0.66,0],[0,0]],"o":[[0,0],[0,1.38],[-1.38,0],[0,0],[0,-0.66],[0,0],[0.66,0]],"v":[[2.498,-1.845],[2.5,0],[0,2.734],[-2.5,0],[-2.502,-1.845],[-1.302,-3.045],[1.298,-3.045]],"c":true}]},{"t":13,"s":[{"i":[[0,-0.66],[0,0],[1.38,0],[0,1.38],[0,0],[-0.66,0],[0,0]],"o":[[0,0],[0,1.38],[-1.38,0],[0,0],[0,-0.66],[0,0],[0.66,0]],"v":[[2.5,-1.3],[2.5,0],[0,2.5],[-2.5,0],[-2.5,-1.3],[-1.3,-2.5],[1.3,-2.5]],"c":true}]}],"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"矩形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"蓝","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[33,33.76,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.8,0.42],[-3.44,-0.79],[-0.09,-1.71],[0,0],[0,0],[1.98,-0.1],[0,0],[0,0],[0,0],[0.62,0.57],[0,0],[0,0],[0,0],[0,0],[0,0],[0.2,1.89],[0,0],[0,0],[0,0]],"o":[[3.44,-0.79],[1.74,0.41],[0,0],[0,0],[0,2],[0,0],[0,0],[0,0],[-0.69,0.52],[0,0],[0,0],[0,0],[0,0],[0,0],[-1.94,0],[0,0],[0,0],[0,0],[0,-1.8]],"v":[[-6.39,-8.971],[6.39,-8.971],[9.49,-5.471],[9.5,-5.271],[9.5,3.249],[5.95,6.989],[5.75,6.999],[3.25,6.999],[0.3,9.209],[-1.95,9.089],[-2.06,8.969],[-2.17,8.849],[-2.21,8.779],[-3.4,6.999],[-5.75,6.999],[-9.48,3.639],[-9.49,3.449],[-9.5,3.249],[-9.5,-5.271]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.266,0.638,1,0.5,0.242,0.595,1,1,0.217,0.552,1],"ix":9}},"s":{"a":0,"k":[-9.5,-9.561],"ix":5},"e":{"a":0,"k":[9.5,9.561],"ix":6},"t":1,"nm":"color","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"路径","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"预合成 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[33,33,0],"ix":2},"a":{"a":0,"k":[33,33,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[70,70,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":9,"s":[110,110,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13,"s":[90,90,100]},{"t":16,"s":[100,100,100]}],"ix":6}},"ao":0,"w":66,"h":66,"ip":0,"op":20,"st":0,"bm":0}],"markers":[]}
|
||||
@ -1,5 +0,0 @@
|
||||
# This is a simple Microlog configuration file
|
||||
microlog.level=DEBUG
|
||||
microlog.appender=LogCatAppender;FileAppender
|
||||
microlog.formatter=PatternFormatter
|
||||
microlog.formatter.PatternFormatter.pattern=%c [%P] %m %T
|
||||
132
app/src/main/assets/notification_style.json
Normal file
132
app/src/main/assets/notification_style.json
Normal file
@ -0,0 +1,132 @@
|
||||
[
|
||||
{
|
||||
"login": {
|
||||
"title": "开启消息通知",
|
||||
"content": "新游上线、互动回复,重要推送不错过",
|
||||
"image": "bg_notification_login_style_1",
|
||||
"styleNo": "样式A",
|
||||
"scenes": "场景1"
|
||||
},
|
||||
"question": {
|
||||
"title": "开启消息通知",
|
||||
"content": "及时查看大神回答",
|
||||
"image": "bg_notification_question_style_1",
|
||||
"styleNo": "样式A",
|
||||
"scenes": "场景2"
|
||||
},
|
||||
"answer": {
|
||||
"title": "开启消息通知",
|
||||
"content": "及时查看点赞与评论",
|
||||
"image": "bg_notification_answer_style_1",
|
||||
"styleNo": "样式A",
|
||||
"scenes": "场景3"
|
||||
},
|
||||
"article": {
|
||||
"title": "开启消息通知",
|
||||
"content": "及时查看点赞与评论",
|
||||
"image": "bg_notification_article_style_1",
|
||||
"styleNo": "样式A",
|
||||
"scenes": "场景4"
|
||||
},
|
||||
"video": {
|
||||
"title": "开启消息通知",
|
||||
"content": "实时获取审核与推荐进度",
|
||||
"image": "bg_notification_video_style_1",
|
||||
"styleNo": "样式A",
|
||||
"scenes": "场景5"
|
||||
},
|
||||
"rating": {
|
||||
"title": "开启消息通知",
|
||||
"content": "成功上墙立即知道",
|
||||
"image": "bg_notification_rating_style_1",
|
||||
"styleNo": "样式A",
|
||||
"scenes": "场景6"
|
||||
},
|
||||
"gift": {
|
||||
"title": "开启消息通知",
|
||||
"content": "新上礼包不再错过",
|
||||
"image": "bg_notification_gift_style_1",
|
||||
"styleNo": "样式A",
|
||||
"scenes": "场景7"
|
||||
},
|
||||
"reserveGame": {
|
||||
"title": "开启消息通知",
|
||||
"content": "新游上线即时体验",
|
||||
"image": "bg_notification_reserve_game_style_1",
|
||||
"styleNo": "样式A",
|
||||
"scenes": "场景8"
|
||||
},
|
||||
"feedback": {
|
||||
"title": "开启消息通知",
|
||||
"content": "及时查看客服回复",
|
||||
"image": "bg_notification_feedback_style_1",
|
||||
"styleNo": "样式A",
|
||||
"scenes": "场景9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"login": {
|
||||
"title": "咦!是新的小伙伴耶!",
|
||||
"content": "打开<font color=\"#1383EB\">通知开关</font>,游戏、礼包、抽奖活动不错过",
|
||||
"image": "bg_notification_login_style_2",
|
||||
"styleNo": "样式B",
|
||||
"scenes": "场景1"
|
||||
},
|
||||
"question": {
|
||||
"title": "发布成功!答案马上来!",
|
||||
"content": "为了第一时间通知您,需要打开<font color=\"#1383EB\">通知开关</font>",
|
||||
"image": "bg_notification_question_style_2",
|
||||
"styleNo": "样式B",
|
||||
"scenes": "场景2"
|
||||
},
|
||||
"answer": {
|
||||
"title": "精彩的回答!大佬牛啤!",
|
||||
"content": "打开<font color=\"#1383EB\">通知开关</font>,可以第一时间收获赞美和感谢哟!",
|
||||
"image": "bg_notification_answer_style_2",
|
||||
"styleNo": "样式B",
|
||||
"scenes": "场景3"
|
||||
},
|
||||
"article": {
|
||||
"title": "发布成功!不愧是你!",
|
||||
"content": "打开<font color=\"#1383EB\">通知开关</font>,可以第一时间收获赞美和互动哟!",
|
||||
"image": "bg_notification_article_style_2",
|
||||
"styleNo": "样式B",
|
||||
"scenes": "场景4"
|
||||
},
|
||||
"video": {
|
||||
"title": "“百万”播放预定!",
|
||||
"content": "<font color=\"#1383EB\">打开通知!</font>第一时间知道审核结果和互动信息哟!",
|
||||
"image": "bg_notification_video_style_2",
|
||||
"styleNo": "样式B",
|
||||
"scenes": "场景5"
|
||||
},
|
||||
"rating": {
|
||||
"title": "这游戏超好玩,我说的!",
|
||||
"content": "想知道有多少人吃下安利?<font color=\"#1383EB\">打开通知</font>,马上知道!",
|
||||
"image": "bg_notification_rating_style_2",
|
||||
"styleNo": "样式B",
|
||||
"scenes": "场景6"
|
||||
},
|
||||
"gift": {
|
||||
"title": "获得道具:神奇的游戏礼包!",
|
||||
"content": "<font color=\"#1383EB\">打开通知!</font>礼包上线,马上知道!",
|
||||
"image": "bg_notification_gift_style_2",
|
||||
"styleNo": "样式B",
|
||||
"scenes": "场景7"
|
||||
},
|
||||
"reserveGame": {
|
||||
"title": "玩最新的游戏,做游戏圈最靓的仔",
|
||||
"content": "<font color=\"#1383EB\">打开通知!</font>游戏上线,更快知道!",
|
||||
"image": "bg_notification_reserve_game_style_2",
|
||||
"styleNo": "样式B",
|
||||
"scenes": "场景8"
|
||||
},
|
||||
"feedback": {
|
||||
"title": "真是重要的反馈!",
|
||||
"content": "感恩有你,光环更精彩!<font color=\"#1383EB\">打开通知</font>,客服回复,马上知道!",
|
||||
"image": "bg_notification_feedback_style_2",
|
||||
"styleNo": "样式B",
|
||||
"scenes": "场景9"
|
||||
}
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
758
app/src/main/assets/privacy_policies.html
Normal file
758
app/src/main/assets/privacy_policies.html
Normal file
@ -0,0 +1,758 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>隐私政策</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.date p {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: "SourceHanSansSC-regular" !important;
|
||||
color: #101010;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin-bottom: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.points {
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.points p {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.introduce p {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.content p b {
|
||||
margin: 6px 0;
|
||||
display: block;
|
||||
}
|
||||
.link-text {
|
||||
color: rgb(19, 131, 235);
|
||||
cursor: pointer;
|
||||
}
|
||||
.link-text a {
|
||||
color: rgb(19, 131, 235);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.left-indent {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.page-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
.red-style {
|
||||
color: red;
|
||||
}
|
||||
.bold-font {
|
||||
font-weight: bold;
|
||||
}
|
||||
span.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.link-text {
|
||||
color: #005ad0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="page-title">欢迎您使用光环助手!</div>
|
||||
|
||||
<div class="introduce">
|
||||
<p>
|
||||
为了向您提供游戏预约、论坛互动交流等相关服务,受制于手机系统限制,我们会申请您的设备信息权限;
|
||||
</p>
|
||||
<p>为了让您正常使用游戏下载和论坛功能,我们会申请您的储存权限;</p>
|
||||
<p>以下为完整《隐私权限政策》</p>
|
||||
<p>
|
||||
光环助手(简称“我们”)深知个人信息对您的重要性,我们将依据《中华人民共和国网络安全法》、《信息安全技术
|
||||
个人信息安全规范》(GB/T
|
||||
35273-2017)以及其他相关法律法规和技术规范收集和使用您的个人信息,以帮助我们向您提供更优质的产品和/或服务,
|
||||
保护您的个人信息及隐私安全。我们制定本“隐私指引”并特别提示:希望您在使用光环助手及相关服务前仔细阅读并理解本隐私政策,以便做出适当的选择。
|
||||
</p>
|
||||
<p>
|
||||
下文将帮您详细了解我们如何收集、使用、存储、传输、共享、转让(如适用)与保护个人信息;帮您了解查询、访问、删除、更正、撤回授权个人信息的方式。其中,
|
||||
<b>
|
||||
有关您个人信息权益的条款重要内容我们已用加粗形式提示,请特别关注。
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="points">
|
||||
<p><b>1.我们处理个人信息的法律依据</b></p>
|
||||
<p><b>2.我们如何共享、转让、公开披露个人信息</b></p>
|
||||
<p><b>3.我们如何收集和使用个人信息</b></p>
|
||||
<p><b>4.我们如何存储个人信息</b></p>
|
||||
<p><b>5.我们如何保护个人信息的安全</b></p>
|
||||
<p><b>6.管理您的个人信息</b></p>
|
||||
<p><b>7.未成年人使用条款</b></p>
|
||||
<p><b>8.隐私政策的修订和通知</b></p>
|
||||
<p><b>9.联系我们</b></p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p><b>1.我们处理个人信息的法律依据</b></p>
|
||||
<p>
|
||||
如果您是中华人民共和国大陆地区的用户,我们将依据《中华人民共和国网络安全法》、《信息安全技术
|
||||
个人信息安全规范》(GB/T
|
||||
35273-2017)以及其他相关法律法规收集和使用您的个人信息,为您提供产品或服务。
|
||||
</p>
|
||||
<p>
|
||||
我们通常只会在征得您同意的情况下收集您的个人信息。
|
||||
在某些情况下,我们可能还会基于法律义务或者履行合同之必需向您收集个人信息,或者可能需要个人信息来保护您的重要利益或其他人的利益。
|
||||
</p>
|
||||
|
||||
<p><b>2.我们如何共享、转让、公开披露个人信息</b></p>
|
||||
|
||||
<p class="title margintop"><b>2.1第三方SDK接入说明</b></p>
|
||||
<p>
|
||||
为保障光环助手App相关功能的实现与应用安全稳定的运行,我们会接入由第三方提供的软件开发包(SDK)实现相关功能。
|
||||
<br />
|
||||
我们会对合作方获取有关信息的软件工具开发包(SDK)进行严格的安全检测,并与授权合作伙伴约定严格的数据保护措施,令其按照我们的委托目的、服务说明、本隐私权政策以及其他任何相关的保密和安全措施来处理个人信息。
|
||||
<br />
|
||||
<span class="red-style">
|
||||
下方为整个光环助手
|
||||
<span class="bold">所有版本</span>
|
||||
内接入的所有信息收集类第三方SDK的权限说明,因隐私政策会因光环助手版本迭代而新接入SDK或停止合作部分SDK,方便照顾
|
||||
<span class="bold">所有版本</span>
|
||||
的用户查看自己SDK第三方权限说明。
|
||||
<br />
|
||||
我们对涉及用户信息使用的SDK相关情况进行了逐项列举,具体如下:
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="margintop red-style bold-font"><b>(1)数据统计类</b></p>
|
||||
<p>1.头条推广</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">
|
||||
https://ad.oceanengine.com/openapi/index.html
|
||||
</span>
|
||||
</p>
|
||||
<p>SDK包名:com.bytedance</p>
|
||||
<p>企业主体:北京有竹居网络技术有限公司</p>
|
||||
<p>使用目的:用于广告流量统计相关服务</p>
|
||||
<p>
|
||||
收集信息类型:设备品牌、型号、软件系统相关信息、安卓(oaid、无线网SSID名称、WiFi路由器MAC地址、设备MAC地址、IMEI、地理位置)
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
https://ad.oceanengine.com/openapi/register/protocol.html?rid=vo25p8sfqde
|
||||
</span>
|
||||
</p>
|
||||
<p>2.talkingdata统计</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">http://www.talkingdata.com/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tendcloud</p>
|
||||
<p>企业主体:北京腾云天下科技有限公司</p>
|
||||
<p>使用目的:用于统计数据和效果分析,以便为用户提供更好的服务</p>
|
||||
<p>收集信息类型:设备信息、网络信息、位置信息、应用信息</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
http://www.talkingdata.com/privacy.jsp?languagetype=zh_cn
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>3.腾讯MTA</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://mta.qq.com/mta/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tencent</p>
|
||||
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
|
||||
<p>使用目的:用于统计数据和效果分析</p>
|
||||
<p>
|
||||
收集信息类型:Mac地址、唯一设备识别码(IMEI、android
|
||||
ID、IDFA、OPENUDID、GUID/SIM卡IMSI信息)、地理位置信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
https://mta.qq.com/mta/ctr_index/protocol_v2/
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>4.腾讯广点通</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://developers.e.qq.com/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tencent</p>
|
||||
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
|
||||
<p>使用目的:用于广告流量统计相关服务</p>
|
||||
<p>
|
||||
收集信息类型:
|
||||
个人常用设备信息(IMEI、AndroidID)、位置信息,IP地址、软件版本号
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">https://e.qq.com/optout.html</span>
|
||||
</p>
|
||||
|
||||
<p class="margintop red-style bold-font"><b>(2)社交登录类</b></p>
|
||||
<p>5.微信登录分享</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://open.weixin.qq.com/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tencent.mm.opensdk</p>
|
||||
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
|
||||
<p>使用目的:用于支持微信登录、分享</p>
|
||||
<p>
|
||||
收集信息类型:个人常用设备信息(MAC地址、IMEI、AndroidID)、硬件型号、操作系统类型、软件信息(软件版本号、浏览器类型)、IP地址、服务日志信息、通讯日志信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">https://privacy.tencent.com/</span>
|
||||
</p>
|
||||
|
||||
<p>6.QQ登录分享</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://connect.qq.com/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tentcent</p>
|
||||
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
|
||||
<p>使用目的:用于支持QQ登录、分享</p>
|
||||
<p>
|
||||
收集信息类型:个人常用设备信息(MAC地址、IMEI、AndroidID、IMSI、ICCID、序列号)、设备型号、操作系统版本、软件信息(软件版本号、浏览器类型)、网络信息、IP地址、服务日志信息、通讯日志信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
https://wiki.connect.qq.com/qq互联sdk隐私保护声明
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>7.微博登录分享</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">http://open.weibo.com/authentication</span>
|
||||
</p>
|
||||
<p>SDK包名:com.sina.weibo.sdk</p>
|
||||
<p>企业主体:北京微梦创科网络技术有限公司</p>
|
||||
<p>使用目的:用于支持微博登录、分享</p>
|
||||
<p>
|
||||
收集信息类型:个人常用设备信息(MAC地址、IMEI、AndroidID、IMSI、ICCID、序列号)、网络信息、应用列表,硬件型号、操作系统类型、软件信息(软件版本号、浏览器类型)、IP地址、服务日志信息、通讯日志信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">https://open.weibo.com/wiki/开发者协议</span>
|
||||
</p>
|
||||
|
||||
<p>8.头条抖音登录</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://open.douyin.com/platform</span>
|
||||
</p>
|
||||
<p>SDK包名:com.bytedance.sdk</p>
|
||||
<p>企业主体:北京字节跳动科技有限公司</p>
|
||||
<p>使用目的:用于支持抖音登录</p>
|
||||
<p>
|
||||
收集信息类型:个人常用设备信息(MAC地址、IMEI、AndroidID)、硬件型号、操作系统类型、软件信息(软件版本号、浏览器类型)、IP地址、服务日志信息、通讯日志信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
https://www.douyin.com/agreements/?id=6773901168964798477
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="margintop red-style bold-font"><b>(3)推送通知类</b></p>
|
||||
<p>9.友盟推送</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://www.umeng.com/push</span>
|
||||
</p>
|
||||
<p>SDK包名:com.umeng</p>
|
||||
<p>企业主体:北京友盟网络科技有限公司</p>
|
||||
<p>使用目的:用于游戏相关信息的提醒通知</p>
|
||||
<p>
|
||||
收集信息类型:Mac地址、唯一设备识别码(IMEI、android
|
||||
ID、IDFA、OPENUDID、GUID/SIM卡IMSI信息)、地理位置信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
https://www.umeng.com/page/policy?spm=a213m0.14063960.0.0.7f626e72hx3nnv
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="margintop red-style bold-font"><b>(4)其他功能类</b></p>
|
||||
<p>10.阿里云反爬虫</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://www.aliyun.com/product/antibot</span>
|
||||
</p>
|
||||
<p>SDK包名:com.alibaba.wireless</p>
|
||||
<p>企业主体:阿里巴巴网络技术有限公司</p>
|
||||
<p>使用目的:为APP提供网络应用安全防护</p>
|
||||
<p>
|
||||
收集信息类型:设备相关信息(例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息)、设备所在位置相关信息(例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息)。
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>11.腾讯bugly</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://bugly.qq.com/v2/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tencent.bugly</p>
|
||||
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
|
||||
<p>使用目的:APP异常上报</p>
|
||||
<p>
|
||||
收集信息类型:设备及应用信息。如:设备名称、设备识别符、硬件型号、操作系统版本、应用程序版本
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">https://bugly.qq.com/v2/contract</span>
|
||||
</p>
|
||||
|
||||
<p>12.阿里云文件上传</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://www.alibabacloud.com/zh</span>
|
||||
</p>
|
||||
<p>SDK包名:com.alibaba.sdk.android</p>
|
||||
<p>SDK包名:com.alibaba.sdk.android</p>
|
||||
<p>企业主体:阿里巴巴网络技术有限公司</p>
|
||||
<p>使用目的:用于支持用户上传视频等相关内容</p>
|
||||
<p>
|
||||
收集信息类型:设备相关信息(例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息)、设备所在位置相关信息(例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息)。
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>13.阿里云日志上传</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://www.alibabacloud.com/zh</span>
|
||||
</p>
|
||||
<p>SDK包名:com.aliyun.sls.android.sdk</p>
|
||||
<p>企业主体:阿里巴巴网络技术有限公司</p>
|
||||
<p>
|
||||
使用目的:通过网络日志分析这些信息以便更及时响应您的帮助请求,以及用于改进服务
|
||||
</p>
|
||||
<p>
|
||||
收集信息类型:设备相关信息(例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息)、设备所在位置相关信息(例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息)。
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>14.容联七陌</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://www.7moor.com/developer</span>
|
||||
</p>
|
||||
<p>SDK包名:com.m7.imkfsdk</p>
|
||||
<p>企业主体:北京七陌科技有限公司</p>
|
||||
<p>使用目的:用于提供对应在线客服功能</p>
|
||||
<p>
|
||||
收集信息类型:设备相关信息(设备名称、设备型号、硬件序列号、操作系统和应用程序版本及类型、语言设置、分辨率、移动终端随机存储内存、摄像头/相册、通讯录权限等)
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
http://m.7moor.com/72/57/p5077783560e807/
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p><b>2.2 共享您的个人信息</b></p>
|
||||
<p>
|
||||
(1)我们不会与任何公司、组织和个人共享您的个人信息,但以下情况除外:
|
||||
</p>
|
||||
<p>
|
||||
(2)事先获得您的明确授权或同意:
|
||||
获得您的明确同意后,我们会与其他方共享您的个人信息;
|
||||
</p>
|
||||
<p>
|
||||
(3)在法定情形下的共享:
|
||||
根据适用的法律法规、法律程序、政府的强制命令或司法裁定而需共享您的个人信息;
|
||||
</p>
|
||||
<p>
|
||||
(4)在法律要求或允许的范围内,为了保护光环助手及其用户或社会公众的利益、财产或安全免遭损害而有必要提供您的个人信息给第三方;
|
||||
</p>
|
||||
<p>
|
||||
(5)与我们的关联公司共享:
|
||||
您的个人信息可能会在我们的关联公司之间共享。我们会对共享的个人信息进行匿名化处理,且这种共享受本指引所声明目的的约束。关联公司如要改变个人信息的处理目的,将再次征求您的授权同意。
|
||||
</p>
|
||||
<p><b>2.3转让</b></p>
|
||||
<p>
|
||||
(1)我们不会转让您的个人信息给任何其他第三方,除非征得您的明确同意。
|
||||
</p>
|
||||
<p>
|
||||
(2)随着我们业务的持续发展,我们将有可能进行合并、收购、资产转让,您的个人信息有可能因此而被转移。在发生前述变更时,我们将按照法律法规及不低于本隐私政策所载明的安全标准要求继受方保护您的个人信息,否则我们将要求继受方重新征得您的授权同意。
|
||||
</p>
|
||||
<p><b>2.4披露</b></p>
|
||||
<p>
|
||||
(1)我们不会公开披露您的信息,除非遵循国家法律法规规定或者获得您的同意。我们公开披露您的个人信息会采用符合行业内标准的安全保护措施。
|
||||
</p>
|
||||
<p>
|
||||
(2)基于法律、法律程序、诉讼或政府主管部门强制性要求的情况下,我们可能会向有权机关披露您的个人信息。但我们保证,在上述情况发生时,我们会要求披露请求方必须出具与之相应的有效法律文件,并对被披露的信息采取符合法律和业界标准的安全防护措施。
|
||||
</p>
|
||||
<p>
|
||||
(3)对违规账号、欺诈行为进行处罚公告时,我们会披露相关账号的信息。
|
||||
</p>
|
||||
|
||||
<p><b>2.5依法豁免征得同意共享、转让、公开披露的个人信息</b></p>
|
||||
<p>
|
||||
请您理解,在下列情形中,根据法律法规及国家标准,我们共享、转让、公开披露您的个人信息无需征得您的授权同意:
|
||||
</p>
|
||||
<p>(1)与国家安全、国防安全直接相关的;</p>
|
||||
<p>(2)与公共安全、公共卫生、重大公共利益直接相关的;</p>
|
||||
<p>(3)与犯罪侦查、起诉、审判和判决执行等直接相关的;</p>
|
||||
<p>
|
||||
(4)出于维护您或其他个人的生命、财产等重大合法权益但又很难得到本人同意的;
|
||||
</p>
|
||||
<p>(5)您自行向社会公众公开的个人信息;</p>
|
||||
<p>
|
||||
(6)从合法公开披露的信息中收集个人信息的,如合法的新闻报道、政府信息公开等渠道。
|
||||
</p>
|
||||
|
||||
<p><b>3.我们如何收集和使用个人信息</b></p>
|
||||
<p>
|
||||
我们会遵循正当、合法、必要的原则,出于本指引所述的以下目的,收集和使用您在使用服务过程中主动提供或因使用产品或服务而产生的个人信息。
|
||||
</p>
|
||||
<p>
|
||||
我们收集和使用的您的个人信息类型包括两种:第一种:我们产品或服务的核心业务功能所必需的信息:此类信息为产品或服务正常运行的必备信息,您须授权我们收集。如您拒绝提供,您将无法正常使用我们的功能,以"仅浏览(游客身份)"
|
||||
的状态体验;第二种:我们产品或服务的附加业务功能可能需要收集的信息:此信息为非核心业务功能所需的信息,您可以选择是否授权我们收集。如您拒绝提供,将导致附加业务功能无法实现或无法达到我们拟达到的效果,但不影响您对核心业务功能的正常使用。
|
||||
</p>
|
||||
<p>
|
||||
如果我们要将您的个人信息用于本指引未载明的其它用途,或基于特定目的将收集而来的信息用于其他目的,我们将以合理的方式向您告知,并在使用前再次征得您的同意。
|
||||
</p>
|
||||
|
||||
<p><b>3.1实现产品或服务的基本功能</b></p>
|
||||
<p>
|
||||
(1)手机管理和内容资源下载功能。为实现手机管理及手机内容资源下载的基本功能,我们会通过手机系统的公用接口收集经过MD5算法加密的国际移动设备身份码(IMEI)和网络设备地址(MAC),以及手机型号、手机系统版本号、系统编号、系统ID号、屏幕分辨率、上网类型、手机中软件的名称、版本号、版本名、包名、软件使用时间和频率、软件崩溃信息、设备和软件相关的信息。这些信息是提供服务所必须收集的基础信息,如您拒绝提供上述权限将可能导致您无法使用我们的服务。
|
||||
</p>
|
||||
<p>
|
||||
(2)软件升级管理功能。为实现手机软件下载、安装、升级、卸载软件管理功能,在您使用产品时,我们会采集您手机中已安装软件的软件名称、版本号、版本名、软件包名信息并上传到我们的服务器进行软件版本比对。发现有更新的版本,我们会提示您升级相应的软件。上述软件信息为实现此功能所必需,不涉及您个人身份敏感信息。
|
||||
</p>
|
||||
<p>
|
||||
(3)过滤无法使用的软件功能。为了过滤您手机无法使用的软件,我们会收集您手机的手机型号、手机系统版本号、系统版本号、屏幕分辨率信息,并依据这些信息排除您手机无法使用的软件,以保证您在光环助手下载的软件都可安装使用。
|
||||
</p>
|
||||
|
||||
<p><b>3.2关于获取手机设备信息的说明</b></p>
|
||||
<p>
|
||||
(1)为方便区分每个用户的个人信息等,本软件需获取用户的手机设备信息,用于游戏主动预约、论坛互动交流后进行推送等用户相关的行为
|
||||
</p>
|
||||
<p>
|
||||
(2)为了保障软件与服务的安全运行,我们会收集您的硬件型号、操作系统版本号、国际移动设备识别码、唯一设备标识符、网络设备硬件地址、IP
|
||||
地址、WLAN接入点、蓝牙、基站、软件版本号、网络接入方式、类型、状态、网络质量数据、操作、使用、服务日志。
|
||||
</p>
|
||||
<p>
|
||||
(3)为了预防恶意程序及安全运营所必需,我们会收集安装的应用信息或正在运行的进程信息、应用程序的总体运行、使用情况与频率、应用崩溃情况、总体安装使用情况、性能数据、应用来源。
|
||||
</p>
|
||||
<p>
|
||||
(4)我们可能使用您的账户信息、设备信息、服务日志信息以及我们关联公司、合作方在获得您授权或依法可以共享的信息,用于判断账户安全、进行身份验证、检测及防范安全事件。
|
||||
</p>
|
||||
<p>(5)具体会发生获取手机设备信息场景如下说明:</p>
|
||||
|
||||
<p class="left-indent">
|
||||
1) 首次启动光环助手
|
||||
<b></b>
|
||||
2) 游戏列表/游戏详情/资讯文章详情/搜索结果页-预约功能
|
||||
<b></b>
|
||||
3) 礼包中心/礼包详情-领取功能
|
||||
<b></b>
|
||||
4) 评论详情-发送评论功能
|
||||
<b></b>
|
||||
5) 回答/问题详情-我来回答功能
|
||||
<b></b>
|
||||
6) 问答首页-提问功能
|
||||
<b></b>
|
||||
7) 个人主页-发文章功能
|
||||
<b></b>
|
||||
8) 帖子草稿/我的草稿-编辑功能
|
||||
<b></b>
|
||||
9) 游戏投稿功能
|
||||
<b></b>
|
||||
10)视频投稿-上传视频功能
|
||||
<b></b>
|
||||
11)游戏详情-关注游戏功能
|
||||
</p>
|
||||
|
||||
<p><b>3.3帮助您成为我们的在线用户</b></p>
|
||||
<p>(1)注册账号/登录账号</p>
|
||||
<p>
|
||||
a.当您注册、登录我们相关服务时,您可以通过手机号创建账号,并且您可以完善相关的网络身份识别信息(头像、昵称、密码),收集这些信息是为了帮助您完成注册。您还可以根据自身需求选择填写性别、生日、地区及个人介绍来完善您的信息。
|
||||
</p>
|
||||
<p>
|
||||
b.您也可以使用第三方账号登录并使用,您将授权我们获取您在第三方平台注册的公开信息(头像、昵称以及您授权的其他信息),用于与光环助手账号绑定,使您可以直接登录并使用本产品和相关服务。
|
||||
</p>
|
||||
<p>(2)认证用户</p>
|
||||
<p>
|
||||
a.在您使用身份认证的功能或服务时,根据相关法律法规,您可能需要提供您的真实身份信息(真实姓名、身份证号码、电话号码)以完成实名验证。
|
||||
</p>
|
||||
<p>
|
||||
b.这些信息属于个人敏感信息,您可以拒绝提供,但您将可能无法获得相关服务,但不影响其他功能与服务的正常使用。
|
||||
</p>
|
||||
|
||||
<p><b>3.4搜索</b></p>
|
||||
<p>
|
||||
(1)您使用“光环助手”的搜索服务时,我们会收集您的搜索关键字信息、日志记录。
|
||||
</p>
|
||||
<p>
|
||||
(2)为了提供高效的搜索服务,部分前述信息会暂时存储在您的本地存储设备之中,并可向您展示搜索结果内容、搜索历史记录。
|
||||
</p>
|
||||
|
||||
<p><b>3.5预约游戏</b></p>
|
||||
<p>
|
||||
当您使用游戏预约、游戏开测提醒功能时,您可以根据需要是否填写手机号。如您拒绝提供,仅会使您无法接收该预约游戏的短信快速提醒功能,但并不影响您正常使用产品与服务的其他。
|
||||
</p>
|
||||
|
||||
<p><b>3.6游戏时长统计</b></p>
|
||||
<p>
|
||||
您可以授权我们使用应用使用记录访问权限,我们会获取您使用某款游戏应用的使用时长,以便于提供游戏时长展示服务以及对应的大数据统计分析。
|
||||
</p>
|
||||
|
||||
<p><b>3.7信息发布功能</b></p>
|
||||
<p>
|
||||
(1)注册成为光环用户后,可在光环平台上发布提问、帖子、视频,并对别人的提问作出回答或邀请其他用户回答,您还可以对别人的回答、帖子和视频的评论作出回复、赞同、感谢。
|
||||
</p>
|
||||
<p>
|
||||
(2)上述功能基于相册(图片库/视频库)的图片/视频访问及上传的附加服务,我们会请求您授权相机、照片、麦克风权限,您可以使用该功能上传您的照片/图片/视频,以实现发布照片/图片/视频的功能、与其他用户进行照片/图片分享等功能。如您拒绝提供该权限和内容的,仅会使您无法使用该功能,但并不影响您正常使用产品与/或服务的其他功能。
|
||||
</p>
|
||||
<p>
|
||||
(3)您发布内容、评论、提问或回答时,我们将收集您发布的信息,并展示您的昵称、头像、发布内容。
|
||||
</p>
|
||||
<p>
|
||||
(4)用户因使用我们的产品或者服务而被我们收集的信息,例如其他用户发布的信息中可能含有您的部分信息(如:在评论、留言、发布图文、音视频中涉及到与您相关的信息)。
|
||||
</p>
|
||||
|
||||
<p><b>3.8浏览、关注与收藏功能</b></p>
|
||||
<p>(1)您可浏览的内容包括问答、评论、专栏、文章。</p>
|
||||
<p>
|
||||
(2)在浏览的过程中,您还可以关注您感兴趣的用户、专栏、问题、收藏,并收藏上述内容。
|
||||
</p>
|
||||
<p>
|
||||
(3)为此,
|
||||
我们可能会收集您使用时的设备信息,如设备型号、唯一设备标识符、操作系统、分辨率、电信运营商等软硬件信息。
|
||||
我们还可能收集您的浏览器类型,以此来为您提供信息展示的最优方案。
|
||||
</p>
|
||||
<p>
|
||||
(4)此外,在您使用浏览和收藏功能的过程中,我们会自动收集您使用的详细情况,并作为有关的
|
||||
网络日志保存,包括但不限于您输入的搜索关键词信息和点击的链接。
|
||||
</p>
|
||||
<p>
|
||||
(5)您浏览和发布的内容及评论信息,您上传的图片信息、您的交易信息、您使用的语言、访问的日期和时间、及您请求的网页记录、操作系统、软件版本号、登录
|
||||
IP 信息。
|
||||
</p>
|
||||
<p>
|
||||
(6)在此过程中,
|
||||
我们会收集您的浏览记录,浏览记录包括您浏览的问答、主页、文章、专栏,
|
||||
您可以自主删除浏览记录。
|
||||
</p>
|
||||
<p><b>3.9互动交流</b></p>
|
||||
<p>
|
||||
(1)您主动关注您感兴趣的账号、内容、视频并与之进行互动,进行浏览、评论、收藏、点赞或分享内容时,我们会收集您关注的账号,并向您展示您关注账号发布内容。
|
||||
</p>
|
||||
<p>
|
||||
(2)您使用推荐通讯录好友功能时,我们会请求通讯录权限,并将通讯录中的信息进行高强度加密算法处理后,用于向您推荐通信录中的好友。通讯录信息属于个人敏感信息,拒绝提供该信息仅会使您无法使用上述功能,但不影响您正常使用“光环助手”及相关服务的其他功能。
|
||||
</p>
|
||||
|
||||
<p><b>3.10收集、使用个人信息目的变更</b></p>
|
||||
<p>
|
||||
(1)请您了解,随着我们业务的发展,可能会对“光环助手”的功能和提供的服务有所调整变化。
|
||||
</p>
|
||||
<p>
|
||||
(2)原则上,当新功能或服务与我们当前提供的功能或服务相关时,收集与使用的个人信息将与原处理目的具有直接或合理关联。
|
||||
</p>
|
||||
<p>
|
||||
(3)在与原处理目的无直接或合理关联的场景下,我们收集、使用您的个人信息,会再次进行告知,并征得您的同意。
|
||||
</p>
|
||||
|
||||
<p><b>3.11依法豁免征得同意收集和使用的个人信息</b></p>
|
||||
<p>
|
||||
请您理解,在下列情形中,根据法律法规及相关国家标准,我们收集和使用您的个人信息无需征得您的授权同意:
|
||||
</p>
|
||||
<p>(1)与国家安全、国防安全直接相关的;</p>
|
||||
<p>(2)与公共安全、公共卫生、重大公共利益直接相关的;</p>
|
||||
<p>(3)与犯罪侦查、起诉、审判和判决执行等直接相关的;</p>
|
||||
<p>
|
||||
(4)出于维护个人信息主体或其他个人的生命、财产等重大合法权益但又很难得到本人同意的;
|
||||
</p>
|
||||
<p>(5)所收集的您的个人信息是您自行向社会公众公开的;</p>
|
||||
<p>
|
||||
(6)从合法公开披露的信息中收集的您的个人信息的,如合法的新闻报道、政府信息公开等渠道;
|
||||
</p>
|
||||
<p>(7)根据您的要求签订或履行合同所必需的;</p>
|
||||
<p>
|
||||
(8)用于维护软件及相关服务的安全稳定运行所必需的,例如发现、处置软件及相关服务的故障;
|
||||
</p>
|
||||
<p>(9)为合法的新闻报道所必需的;</p>
|
||||
<p>
|
||||
(10)学术研究机构基于公共利益开展统计或学术研究所必要,且对外提供学术研究或描述的结果时,对结果中所包含的个人信息进行去标识化处理的。
|
||||
</p>
|
||||
<p>(11)法律法规规定的其他情形。</p>
|
||||
<p>
|
||||
特别提示您注意,如信息无法单独或结合其他信息识别到您的个人身份,其不属于法律意义上您的个人信息;当您的信息可以单独或结合其他信息识别到您的个人身份时或我们将无法与任何特定个人信息建立联系的数据与其他您的个人信息结合使用时,这些信息在结合使用期间,将作为您的个人信息按照本隐私政策处理与保护。
|
||||
</p>
|
||||
|
||||
<p><b>4.我们如何存储个人信息</b></p>
|
||||
<p><b>4.1 存储地点</b></p>
|
||||
<p>
|
||||
(1)我们依照法律法规的规定,将在境内运营过程中收集和产生的您的个人信息存储于中华人民共和国境内。
|
||||
</p>
|
||||
<p>
|
||||
(2)目前,我们不会将上述信息传输至境外,如果我们向境外传输,我们将会遵循相关国家规定或者征求您的同意。
|
||||
</p>
|
||||
<p><b>4.2存储期限</b></p>
|
||||
<p>
|
||||
(1)我们仅在为提供“光环助手”及服务之目的所必需的期间内保留您的个人信息:您发布的信息、评论、点赞及相关信息,在您未撤回、删除或未注销账号期间,我们会保留相关信息。
|
||||
</p>
|
||||
<p>
|
||||
(2)超出必要期限后,我们将对您的个人信息进行删除或匿名化处理,但法律法规另有规定的除外。
|
||||
</p>
|
||||
|
||||
<p><b>5.我们如何保护个人信息的安全</b></p>
|
||||
<p>
|
||||
(1)我们非常重视您个人信息的安全,将努力采取合理的安全措施(包括技术方面和管理方面)来保护您的个人信息,防止您提供的个人信息被不当使用或未经授权的情况下被访问、公开披露、使用、修改、损坏、丢失或泄漏。
|
||||
</p>
|
||||
<p>
|
||||
(2)我们会使用不低于行业同行的加密技术、匿名化处理及相关合理可行的手段保护您的个人信息,并使用安全保护机制防止您的个人信息遭到恶意攻击。
|
||||
</p>
|
||||
<p>
|
||||
(3)我们会建立专门的安全部门、安全管理制度、数据安全流程保障您的个人信息安全。我们采取严格的数据使用和访问制度,确保只有授权人员才可访问您的个人信息,并适时对数据和技术进行安全审计。
|
||||
</p>
|
||||
<p>
|
||||
(4)尽管已经采取了上述合理有效措施,并已经遵守了相关法律规定要求的标准,但请您理解,由于技术的限制以及可能存在的各种恶意手段,在互联网行业,即便竭尽所能加强安全措施,也不可能始终保证信息百分之百的安全,我们将尽力确保您提供给我们的个人信息的安全性。
|
||||
</p>
|
||||
<p>
|
||||
(5)您知悉并理解,您接入我们的服务所用的系统和通讯网络,有可能因我们可控范围外的因素而出现问题。因此,我们强烈建议您采取积极措施保护个人信息的安全,包括但不限于使用复杂密码、定期修改密码、不将自己的账号密码及相关个人信息透露给他人。
|
||||
</p>
|
||||
<p>
|
||||
(6)我们会制定应急处理预案,并在发生用户信息安全事件时立即启动应急预案,努力阻止这些安全事件的影响和后果扩大。一旦发生用户信息安全事件(泄露、丢失)后,我们将按照法律法规的要求,及时向您告知:安全事件的基本情况和可能的影响、我们已经采取或将要采取的处置措施、您可自主防范和降低风险的建议、对您的补救措施。我们将及时将事件相关情况以推送通知、邮件、信函、短信及相关形式告知您,难以逐一告知时,我们会采取合理、有效的方式发布公告。同时,我们还将按照相关监管部门要求,上报用户信息安全事件的处置情况。
|
||||
</p>
|
||||
<p>
|
||||
(7)您一旦离开“光环助手”及相关服务,浏览或使用其他网站、服务及内容资源,我们将没有能力和直接义务保护您在光环助手及相关服务之外的软件、网站提交的任何个人信息,无论您登录、浏览或使用上述软件、网站是否基于“光环助手”的链接或引导。
|
||||
</p>
|
||||
|
||||
<p><b>6.管理您的个人信息</b></p>
|
||||
<p>
|
||||
我们非常重视您对个人信息的管理,并尽全力保护您的隐私,对于您个人信息的查询、访问、修改、删除、撤回同意授权、注销账号、投诉举报以及设置隐私功能的相关权利,以使您有能力保障您的隐私和信息安全。
|
||||
</p>
|
||||
|
||||
<p><b>6.1 访问、删除、更正您的个人信息</b></p>
|
||||
<p>(1)访问个人账号信息</p>
|
||||
<p>a. 您可以查询、访问您的头像、用户名、简介、性别、生日、地区</p>
|
||||
<p>b.您可以在光环助手的“个人中心”中进行查询、访问。</p>
|
||||
<p>(2)查询访问、更正、取消您关注账号、查询访问粉丝、访客信息</p>
|
||||
<p>a.进入“关注”在关注列表中查询、访问、取消关注您关注的账号。</p>
|
||||
<p>
|
||||
(3)查询访问、更改、删除您的收藏、点赞、浏览记录、阅读历史记录、搜索历史历史记录
|
||||
</p>
|
||||
<p>
|
||||
a.点击“我的”—点击“我的收藏”、
|
||||
“我的点赞”、或“浏览历史”进入查询访问、删除;
|
||||
</p>
|
||||
<p>b.点击搜索栏—删除搜索“历史记录”</p>
|
||||
<p>c.您可以通过点击“系统设置”—点击“清理缓存”。</p>
|
||||
<p>(4)投诉举报</p>
|
||||
<p>a.您可按照我们公示的制度进行投诉或举报。</p>
|
||||
<p>
|
||||
b.如果您认为您的个人信息权利可能受到侵害,或者发现侵害个人信息权利的线索(例如:认为我们收集您的个人信息违反法律规定或者双方约定),“我的”—“基础功能”—“用户反馈”,进入用户反馈界面与我们联系。
|
||||
</p>
|
||||
<p>c.我们核查后会及时反馈您的投诉与举报。</p>
|
||||
<p>(5)访问隐私政策</p>
|
||||
<p>
|
||||
a.您可以在注册页面,或者在登录个人账号“设置”—“关于”查看本隐私政策的全部内容
|
||||
</p>
|
||||
<p>
|
||||
b.请您了解,本隐私政策中所述的“光环助手”及相关服务可能会根据您所使用的手机型号、系统版本、软件应用程序版本、移动客户端等因素而有所不同。最终的产品和服务以您所使用的“光环助手”软件及相关服务为准。
|
||||
</p>
|
||||
<p>(6)停止运营向您告知</p>
|
||||
<p>
|
||||
a.如我们停止运营,我们将及时停止收集您个人信息的活动,将停止运营的通知以逐一送达或公告的形式通知您,并对所持有的您的个人信息进行删除或匿名化处理。
|
||||
</p>
|
||||
|
||||
<p><b>6.2 注销您的个人账号</b></p>
|
||||
<p>
|
||||
如需要注销个人账户,可前往光环助手,我的光环> 设置 > 账号与安全 >
|
||||
账号安全中心 >
|
||||
注销账号,进行注销操作。请您注意,如果您选择注销光环助手账户,那么您的光环助手账号将不可被使用且相关账号信息将被删除,包括所发布的所有内容,包括:提问、回答、社区文章、评论、关注的人等均会被清空;您将无法再通过光环助手账号登录光环助手的服务(但不会影响您使用无需账号登录即可使用的服务和功能)
|
||||
</p>
|
||||
<p>
|
||||
当您注销账户后,除法律法规要求我们保存相关信息的情况外,我们将停止为您提供相应的产品(或服务),并在60个工作日内删除或匿名化您的个人信息。
|
||||
</p>
|
||||
|
||||
<p><b>7.未成年人条款</b></p>
|
||||
<p>
|
||||
a.若您是未满18周岁的未成年人,在使用“光环助手”及相关服务前,应在您的父母或其他监护人监护、指导下共同阅读并同意本隐私政策。
|
||||
</p>
|
||||
<p>
|
||||
b.我们根据国家相关法律法规的规定保护未成年人的个人信息,只会在法律允许、父母或其他监护人明确同意或保护未成年人所必要的情况下收集、使用、储存、共享、转让或披露未成年人的个人信息;如果我们发现在未事先获得可证实的父母同意的情况下收集了未成年人的个人信息,则会设法尽快删除相关信息。
|
||||
</p>
|
||||
<p>
|
||||
c.若您是未成年人的监护人,当您对您所监护的未成年人的个人信息有相关疑问时,请通过公司本隐私政策公示的联系方式与我们联系。
|
||||
</p>
|
||||
<p><b>8.隐私政策的修订和通知</b></p>
|
||||
<p>
|
||||
(1)为了给您提供更好的服务,光环助手及相关服务将不时更新与变化,我们会适时对本隐私政策进行修订,这些修订构成本隐私政策的一部分并具有等同于本隐私政策的效力,未经您明确同意,我们不会削减您依据当前生效的本隐私政策所应享受的权利。
|
||||
</p>
|
||||
<p>
|
||||
(2)本隐私政策更新后,我们会在光环助手发出更新版本,并在更新后的条款生效前通过公告或其他适当的方式提醒您更新的内容,以便您及时了解本隐私政策的最新版本。
|
||||
</p>
|
||||
<p><b>9.联系我们</b></p>
|
||||
<p>
|
||||
如果您对我们的隐私政策及对您个人信息的处理有任何疑问、意见、建议、或投诉,请通过以下方式与我们联系
|
||||
</p>
|
||||
<p>广州加兔网络科技有限公司</p>
|
||||
<p>注册地址:广州市番禺区市桥街丹山村青云一街2号229房</p>
|
||||
<p>在线客服QQ:350473523</p>
|
||||
<p>信息保护事务联系电话:020-85526920</p>
|
||||
<p>在一般情况下,我们会在15个工作日内对您的请求予以答复</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -624,3 +624,18 @@ function customLinkgo(self) {
|
||||
// console.log(datas)
|
||||
window.OnLinkClickListener.onClick(datas)
|
||||
}
|
||||
|
||||
// 在web页面播放视频
|
||||
//RE.initArticleVideo = function(){
|
||||
// initArticleVideo()
|
||||
//}
|
||||
|
||||
function showNativeDialog(title, message, positive, negative, callback) {
|
||||
var jsCallbackCode = "(" + function (v) {
|
||||
window.onNativeDialogCallback(v);
|
||||
delete window.onNativeDialogCallback;
|
||||
}.toString() + ")";
|
||||
|
||||
window.onNativeDialogCallback = callback;
|
||||
window.NativeCallBack.showDialog(title, message, positive, negative, jsCallbackCode);
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
493
app/src/main/assets/user_regulation.html
Normal file
493
app/src/main/assets/user_regulation.html
Normal file
@ -0,0 +1,493 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>光环助手软件许可及服务协议</title>
|
||||
</head>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.top {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
.margintop {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.left-indent {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.red-style {
|
||||
color: red;
|
||||
}
|
||||
.bold-font {
|
||||
font-weight: bold;
|
||||
}
|
||||
span.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.link-text {
|
||||
color: #005ad0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<h3 class="top">光环助手软件许可及服务协议</h3>
|
||||
<h5 class="title">首部及导言</h5>
|
||||
<p>欢迎使用光环助手软件许可及服务</p>
|
||||
<p>
|
||||
各位用户在使用光环助手前,请您务必审慎阅读、并充分理解本协议中的各项条款,
|
||||
<span class="bold">
|
||||
特别是免除或者限制责任的条款,以及开通或使用某项服务的单独协议,并选择接受或不接受。
|
||||
</span>
|
||||
除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本软件及相关服务。您的下载、安装、使用、登录等行为即视为您已阅读并同意上述协议的约束。
|
||||
</p>
|
||||
<p>如果您未满18周岁,请在法定监护人的陪同下阅读本协议及其他上述协议。</p>
|
||||
<h5 class="title margintop">一、权利声明</h5>
|
||||
<p>
|
||||
“光环助手”的一切知识产权,以及与“光环助手”相关的所有信息内容,包括但不限于:文字表述及其组合、图标、图饰、图像、图表、色彩、界面设计、版面框架、有关数据、附加程序、印刷材料或电子文档等均为光环助手所有,受著作权法和国际著作权条约以及其他知识产权法律法规的保护。
|
||||
</p>
|
||||
<h5 class="title margintop">二、软件使用规范</h5>
|
||||
<p>
|
||||
2.1
|
||||
本软件是基于Android(安卓)系统手机、平板电脑(PAD)等设备开发的一款软件,提供注册登录、手机游戏管理、游戏推荐、文章阅读等功能
|
||||
</p>
|
||||
<p>2.2 软件的下载、安装和使用</p>
|
||||
<p>
|
||||
本软件为免费软件,用户可以非商业性、无限制数量地从光环授权的渠道下载、安装及使用本软件。
|
||||
</p>
|
||||
<p>
|
||||
<span class="bold">
|
||||
如果您从未经光环授权的第三方获取本软件或与本软件名称相同的安装程序,光环无法保证该软件能够正常使用,并对因此给您造成的损失不予负责。
|
||||
</span>
|
||||
</p>
|
||||
<p>2.3 软件的复制、分发和传播</p>
|
||||
<p>
|
||||
本产品以学习、研究交流为目的。用户可以非商业性、无限制数量地复制、分发和传播本软件产品。但必须保证每一份复制、分发和传播都是完整和真实的,
|
||||
包括所有有关本软件产品的软件、电子文档, 版权和商标,亦包括本协议。
|
||||
</p>
|
||||
<p>2.4 软件的更新</p>
|
||||
<p>
|
||||
为了改善用户体验、完善服务内容,光环将不断努力开发新的服务,并为您不时提供软件更新(这些更新可能会采取软件替换、修改、功能强化、版本升级等形式)。为了保证本软件及服务的安全性和功能的一致性,光环有权不经向您特别通知而对软件进行更新,或者对软件的部分功能效果进行改变或限制。本软件新版本发布后,旧版本的软件可能无法使用。光环不保证旧版本软件继续可用及相应的客户服务,请您随时核对并下载最新版本。
|
||||
</p>
|
||||
<h5 class="title margintop">三、用户使用须知</h5>
|
||||
<p>3.1 您理解并同意:</p>
|
||||
<p>
|
||||
为了向您提供有效的服务,本软件会利用您移动通讯终端的处理器和带宽等资源。本软件使用过程中可能产生数据流量的费用,用户需自行向运营商了解相关资费信息,并自行承担相关费用.
|
||||
</p>
|
||||
<p>3.2 您理解并同意:</p>
|
||||
<p>
|
||||
由本软件进行收录、推荐并提供下载、升级服务的第三方软件,由第三方享有一切合法权利,光环并不能识别用户利用本软件下载、安装的第三方软件是否有合法来源。
|
||||
<span class="bold">
|
||||
因第三方软件引发的任何纠纷,由该第三方负责解决,光环不承担任何责任。
|
||||
</span>
|
||||
同时光环不对第三方软件或技术提供客服支持,若用户需要获取支持,请与该软件或技术提供商联系,若您为有关软件的权利人,不愿本软件为您的软件提供用户下载、安装、使用的服务,也可按本协议约定的联系方式联系我们,我们将会积极配合进行处理。
|
||||
</p>
|
||||
<p>3.3 您理解并同意:</p>
|
||||
<p>
|
||||
<span class="bold">
|
||||
如果因您不正当使用本软件造成了不良影响,或因使用本软件造成的包括但不限于数据异常等问题,均由使用者自行承担,光环团队不对任意类型的使用结果承担责任;
|
||||
</span>
|
||||
</p>
|
||||
<p>3.4 您理解并同意:</p>
|
||||
<p>
|
||||
本软件不含任何破坏用户移动通讯设备数据和获取用户隐私信息的恶意代码,不会泄露用户的个人信息和隐私;
|
||||
</p>
|
||||
<p>3.5 您理解并同意:</p>
|
||||
<p>
|
||||
<span class="bold">
|
||||
对于包括但不限于互联网网络故障、计算机故障、手机故障或病毒、信息损坏或丢失、计算机系统问题,或其它任何基于不可抗力原因而产生的损失,光环团队不承担任何责任。
|
||||
</span>
|
||||
</p>
|
||||
<p>3.6 您理解并同意:</p>
|
||||
<p>光环发布、收录的文章均不代表光环立场。</p>
|
||||
<p>3.7 您理解并同意:</p>
|
||||
<p>
|
||||
为实现软件包括但不限于集中展示、下载、安装、卸载等游戏管理功能以及文章优先推荐功能,本软件会检测用户手机中已安装游戏的包名、版本号、版本名、游戏名称信息。除征得用户明确同意和法律明确规定外,光环不会向第三方泄露任何的用户信息
|
||||
</p>
|
||||
<p>3.8 您理解并同意:</p>
|
||||
<p>
|
||||
用户应在遵守法律及本协议的前提下使用本软件。用户无权实施包括但不限于下列行为:
|
||||
</p>
|
||||
3.8.1 不得删除或者改变本软件上的所有权利管理电子信息
|
||||
<br />
|
||||
3.8.2 不得故意避开或者破坏著作权人为保护本软件著作权而采取的技术措施;
|
||||
<br />
|
||||
3.8.3 用户不得利用本软件误导、欺骗他人;
|
||||
<br />
|
||||
3.8.4
|
||||
违反国家规定,对计算机信息系统功能进行删除、修改、增加、干扰,造成计算机信息系统不能正常运行;
|
||||
<br />
|
||||
3.8.5 未经允许,进入计算机信息网络或者使用计算机信息网络资源;
|
||||
<br />
|
||||
3.8.6 未经允许,对计算机信息网络功能进行删除、修改或者增加;
|
||||
<br />
|
||||
3.8.7
|
||||
未经允许,对计算机信息网络中存储、处理或者传输的数据和应用程序进行删除、修改或者增加;
|
||||
<br />
|
||||
3.8.8 破坏本软件系统或网站的正常运行,故意传播计算机病毒等破坏性程序;
|
||||
<br />
|
||||
3.8.9 其他任何危害计算机信息网络安全的行为。
|
||||
<br />
|
||||
<p>3.9 您理解并同意:</p>
|
||||
<p>
|
||||
本软件经过详细的测试,但不能保证与所有的软硬件系统完全兼容,不能保证本软件完全没有错误。如果出现不兼容及软件错误的情况,用户可通过各反馈途径将情况告知光环团队,获得技术支持。如果无法解决兼容性问题,用户可以删除本软件。
|
||||
</p>
|
||||
<h5 class="title margintop">四、争议解决处理</h5>
|
||||
<p>
|
||||
本《协议》的解释、效力及纠纷的解决,适用于中华人民共和国法律。若用户和光环助手之间发生任何纠纷或争议,首先应友好协商解决,协商不成的,用户在此完全同意将纠纷或争议提交光环助手所在地法院管辖
|
||||
</p>
|
||||
|
||||
<p class="title margintop"><b>五、第三方SDK接入说明</b></p>
|
||||
<p>
|
||||
为保障光环助手App相关功能的实现与应用安全稳定的运行,我们会接入由第三方提供的软件开发包(SDK)实现相关功能。
|
||||
<br />
|
||||
我们会对合作方获取有关信息的软件工具开发包(SDK)进行严格的安全检测,并与授权合作伙伴约定严格的数据保护措施,令其按照我们的委托目的、服务说明、本隐私权政策以及其他任何相关的保密和安全措施来处理个人信息。
|
||||
<br />
|
||||
<span class="red-style">
|
||||
下方为整个光环助手
|
||||
<span class="bold">所有版本</span>
|
||||
内接入的所有信息收集类第三方SDK的权限说明,因隐私政策会因光环助手版本迭代而新接入SDK或停止合作部分SDK,方便照顾
|
||||
<span class="bold">所有版本</span>
|
||||
的用户查看自己SDK第三方权限说明。
|
||||
<br />
|
||||
我们对涉及用户信息使用的SDK相关情况进行了逐项列举,具体如下:
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="margintop red-style bold-font"><b>(1)数据统计类</b></p>
|
||||
<p>1.头条推广</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">
|
||||
https://ad.oceanengine.com/openapi/index.html
|
||||
</span>
|
||||
</p>
|
||||
<p>SDK包名:com.bytedance</p>
|
||||
<p>企业主体:北京有竹居网络技术有限公司</p>
|
||||
<p>使用目的:用于广告流量统计相关服务</p>
|
||||
<p>
|
||||
收集信息类型:设备品牌、型号、软件系统相关信息、安卓(oaid、无线网SSID名称、WiFi路由器MAC地址、设备MAC地址、IMEI、地理位置)
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
https://ad.oceanengine.com/openapi/register/protocol.html?rid=vo25p8sfqde
|
||||
</span>
|
||||
</p>
|
||||
<p>2.talkingdata统计</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">http://www.talkingdata.com/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tendcloud</p>
|
||||
<p>企业主体:北京腾云天下科技有限公司</p>
|
||||
<p>使用目的:用于统计数据和效果分析,以便为用户提供更好的服务</p>
|
||||
<p>收集信息类型:设备信息、网络信息、位置信息、应用信息</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
http://www.talkingdata.com/privacy.jsp?languagetype=zh_cn
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>3.腾讯MTA</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://mta.qq.com/mta/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tencent</p>
|
||||
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
|
||||
<p>使用目的:用于统计数据和效果分析</p>
|
||||
<p>
|
||||
收集信息类型:Mac地址、唯一设备识别码(IMEI、android
|
||||
ID、IDFA、OPENUDID、GUID/SIM卡IMSI信息)、地理位置信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
https://mta.qq.com/mta/ctr_index/protocol_v2/
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>4.腾讯广点通</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://developers.e.qq.com/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tencent</p>
|
||||
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
|
||||
<p>使用目的:用于广告流量统计相关服务</p>
|
||||
<p>
|
||||
收集信息类型:
|
||||
个人常用设备信息(IMEI、AndroidID)、位置信息,IP地址、软件版本号
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">https://e.qq.com/optout.html</span>
|
||||
</p>
|
||||
|
||||
<p class="margintop red-style bold-font"><b>(2)社交登录类</b></p>
|
||||
<p>5.微信登录分享</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://open.weixin.qq.com/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tencent.mm.opensdk</p>
|
||||
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
|
||||
<p>使用目的:用于支持微信登录、分享</p>
|
||||
<p>
|
||||
收集信息类型:个人常用设备信息(MAC地址、IMEI、AndroidID)、硬件型号、操作系统类型、软件信息(软件版本号、浏览器类型)、IP地址、服务日志信息、通讯日志信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">https://privacy.tencent.com/</span>
|
||||
</p>
|
||||
|
||||
<p>6.QQ登录分享</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://connect.qq.com/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tentcent</p>
|
||||
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
|
||||
<p>使用目的:用于支持QQ登录、分享</p>
|
||||
<p>
|
||||
收集信息类型:个人常用设备信息(MAC地址、IMEI、AndroidID、IMSI、ICCID、序列号)、设备型号、操作系统版本、软件信息(软件版本号、浏览器类型)、网络信息、IP地址、服务日志信息、通讯日志信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
https://wiki.connect.qq.com/qq互联sdk隐私保护声明
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>7.微博登录分享</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">http://open.weibo.com/authentication</span>
|
||||
</p>
|
||||
<p>SDK包名:com.sina.weibo.sdk</p>
|
||||
<p>企业主体:北京微梦创科网络技术有限公司</p>
|
||||
<p>使用目的:用于支持微博登录、分享</p>
|
||||
<p>
|
||||
收集信息类型:个人常用设备信息(MAC地址、IMEI、AndroidID、IMSI、ICCID、序列号)、网络信息、应用列表,硬件型号、操作系统类型、软件信息(软件版本号、浏览器类型)、IP地址、服务日志信息、通讯日志信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">https://open.weibo.com/wiki/开发者协议</span>
|
||||
</p>
|
||||
|
||||
<p>8.头条抖音登录</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://open.douyin.com/platform</span>
|
||||
</p>
|
||||
<p>SDK包名:com.bytedance.sdk</p>
|
||||
<p>企业主体:北京字节跳动科技有限公司</p>
|
||||
<p>使用目的:用于支持抖音登录</p>
|
||||
<p>
|
||||
收集信息类型:个人常用设备信息(MAC地址、IMEI、AndroidID)、硬件型号、操作系统类型、软件信息(软件版本号、浏览器类型)、IP地址、服务日志信息、通讯日志信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
https://www.douyin.com/agreements/?id=6773901168964798477
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="margintop red-style bold-font"><b>(3)推送通知类</b></p>
|
||||
<p>9.友盟推送</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://www.umeng.com/push</span>
|
||||
</p>
|
||||
<p>SDK包名:com.umeng</p>
|
||||
<p>企业主体:北京友盟网络科技有限公司</p>
|
||||
<p>使用目的:用于游戏相关信息的提醒通知</p>
|
||||
<p>
|
||||
收集信息类型:Mac地址、唯一设备识别码(IMEI、android
|
||||
ID、IDFA、OPENUDID、GUID/SIM卡IMSI信息)、地理位置信息
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
https://www.umeng.com/page/policy?spm=a213m0.14063960.0.0.7f626e72hx3nnv
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="margintop red-style bold-font"><b>(4)其他功能类</b></p>
|
||||
<p>10.阿里云反爬虫</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://www.aliyun.com/product/antibot</span>
|
||||
</p>
|
||||
<p>SDK包名:com.alibaba.wireless</p>
|
||||
<p>企业主体:阿里巴巴网络技术有限公司</p>
|
||||
<p>使用目的:为APP提供网络应用安全防护</p>
|
||||
<p>
|
||||
收集信息类型:设备相关信息(例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息)、设备所在位置相关信息(例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息)。
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>11.腾讯bugly</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://bugly.qq.com/v2/</span>
|
||||
</p>
|
||||
<p>SDK包名:com.tencent.bugly</p>
|
||||
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
|
||||
<p>使用目的:APP异常上报</p>
|
||||
<p>
|
||||
收集信息类型:设备及应用信息。如:设备名称、设备识别符、硬件型号、操作系统版本、应用程序版本
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">https://bugly.qq.com/v2/contract</span>
|
||||
</p>
|
||||
|
||||
<p>12.阿里云文件上传</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://www.alibabacloud.com/zh</span>
|
||||
</p>
|
||||
<p>SDK包名:com.alibaba.sdk.android</p>
|
||||
<p>SDK包名:com.alibaba.sdk.android</p>
|
||||
<p>企业主体:阿里巴巴网络技术有限公司</p>
|
||||
<p>使用目的:用于支持用户上传视频等相关内容</p>
|
||||
<p>
|
||||
收集信息类型:设备相关信息(例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息)、设备所在位置相关信息(例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息)。
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>13.阿里云日志上传</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://www.alibabacloud.com/zh</span>
|
||||
</p>
|
||||
<p>SDK包名:com.aliyun.sls.android.sdk</p>
|
||||
<p>企业主体:阿里巴巴网络技术有限公司</p>
|
||||
<p>
|
||||
使用目的:通过网络日志分析这些信息以便更及时响应您的帮助请求,以及用于改进服务
|
||||
</p>
|
||||
<p>
|
||||
收集信息类型:设备相关信息(例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息)、设备所在位置相关信息(例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息)。
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">
|
||||
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>14.容联七陌</p>
|
||||
<p>
|
||||
SDK官网:
|
||||
<span class="link-text">https://www.7moor.com/developer</span>
|
||||
</p>
|
||||
<p>SDK包名:com.m7.imkfsdk</p>
|
||||
<p>企业主体:北京七陌科技有限公司</p>
|
||||
<p>使用目的:用于提供对应在线客服功能</p>
|
||||
<p>
|
||||
收集信息类型:设备相关信息(设备名称、设备型号、硬件序列号、操作系统和应用程序版本及类型、语言设置、分辨率、移动终端随机存储内存、摄像头/相册、通讯录权限等)
|
||||
</p>
|
||||
<p>
|
||||
隐私政策链接:
|
||||
<span class="link-text">http://m.7moor.com/72/57/p5077783560e807/</span>
|
||||
</p>
|
||||
|
||||
<h5 class="title margintop">六、关于获取手机设备信息的说明</h5>
|
||||
<div>
|
||||
(1)为方便区分每个用户的个人信息等,本软件需获取用户的手机设备信息,用于游戏主动预约、论坛互动交流后进行推送等用户相关的行为
|
||||
<br />
|
||||
(2)为了保障软件与服务的安全运行,我们会收集您的硬件型号、操作系统版本号、国际移动设备识别码、唯一设备标识符、网络设备硬件地址、IP
|
||||
地址、WLAN接入点、蓝牙、基站、软件版本号、网络接入方式、类型、状态、网络质量数据、操作、使用、服务日志。
|
||||
<br />
|
||||
(3)为了预防恶意程序及安全运营所必需,我们会收集安装的应用信息或正在运行的进程信息、应用程序的总体运行、使用情况与频率、应用崩溃情况、总体安装使用情况、性能数据、应用来源。
|
||||
<br />
|
||||
(4)我们可能使用您的账户信息、设备信息、服务日志信息以及我们关联公司、合作方在获得您授权或依法可以共享的信息,用于判断账户安全、进行身份验证、检测及防范安全事件。
|
||||
<br />
|
||||
(5)具体会发生获取手机设备信息场景如下说明:
|
||||
<br />
|
||||
<p class="left-indent">
|
||||
1) 首次启动光环助手
|
||||
<br />
|
||||
2) 游戏列表/游戏详情/资讯文章详情/搜索结果页-预约功能
|
||||
<br />
|
||||
3) 礼包中心/礼包详情-领取功能
|
||||
<br />
|
||||
4) 评论详情-发送评论功能
|
||||
<br />
|
||||
5) 回答/问题详情-我来回答功能
|
||||
<br />
|
||||
6) 问答首页-提问功能
|
||||
<br />
|
||||
7) 个人主页-发文章功能
|
||||
<br />
|
||||
8) 帖子草稿/我的草稿-编辑功能
|
||||
<br />
|
||||
9) 游戏投稿功能
|
||||
<br />
|
||||
10) 视频投稿-上传视频功能
|
||||
<br />
|
||||
11) 游戏详情-关注游戏功能
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5 class="title margintop">七、其他</h5>
|
||||
<p>
|
||||
7.1
|
||||
本协议所有条款的标题仅为阅读方便,本身并无实际涵义,不能作为本协议涵义解释的依据。
|
||||
<br />
|
||||
7.2
|
||||
如果本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,或违反任何适用的法律,则该条款被视为删除,但本协议的其余条款仍应有效并且有约束力。
|
||||
<br />
|
||||
7.3
|
||||
光环有权随时根据有关法律、法规的变化以及公司经营状况和经营策略的调整等修改本协议。修改后的协议会在软件设置内发布。
|
||||
当发生有关争议时,以最新的协议文本为准。如果不同意改动的内容,用户可以自行删除本软件。如果用户继续使用本软件,则视为您接受本协议的变动。
|
||||
<br />
|
||||
<span class="bold">
|
||||
7.4 光环在法律允许的最大范围内对本协议拥有解释权与修改权。
|
||||
</span>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 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 androidx.swiperefreshlayout.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RadialGradient;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.OvalShape;
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
/**
|
||||
* Private class created to work around issues with AnimationListeners being
|
||||
* called before the animation is actually complete and support shadows on older
|
||||
* platforms.
|
||||
*/
|
||||
class CircleImageView extends ImageView {
|
||||
|
||||
private static final int KEY_SHADOW_COLOR = 0x1E000000;
|
||||
private static final int FILL_SHADOW_COLOR = 0x3D000000;
|
||||
// PX
|
||||
private static final float X_OFFSET = 0f;
|
||||
private static final float Y_OFFSET = 1.75f;
|
||||
private static final float SHADOW_RADIUS = 3.5f;
|
||||
private static final int SHADOW_ELEVATION = 4;
|
||||
|
||||
private Animation.AnimationListener mListener;
|
||||
int mShadowRadius;
|
||||
|
||||
CircleImageView(Context context, int color) {
|
||||
super(context);
|
||||
final float density = getContext().getResources().getDisplayMetrics().density;
|
||||
final int shadowYOffset = (int) (density * Y_OFFSET);
|
||||
final int shadowXOffset = (int) (density * X_OFFSET);
|
||||
|
||||
mShadowRadius = (int) (density * SHADOW_RADIUS);
|
||||
|
||||
ShapeDrawable circle;
|
||||
if (elevationSupported()) {
|
||||
circle = new ShapeDrawable(new OvalShape());
|
||||
ViewCompat.setElevation(this, SHADOW_ELEVATION * density);
|
||||
} else {
|
||||
OvalShape oval = new OvalShadow(mShadowRadius);
|
||||
circle = new ShapeDrawable(oval);
|
||||
setLayerType(View.LAYER_TYPE_SOFTWARE, circle.getPaint());
|
||||
circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset,
|
||||
KEY_SHADOW_COLOR);
|
||||
final int padding = mShadowRadius;
|
||||
// set padding so the inner image sits correctly within the shadow.
|
||||
setPadding(padding, padding, padding, padding);
|
||||
}
|
||||
circle.getPaint().setColor(color);
|
||||
ViewCompat.setBackground(this, circle);
|
||||
}
|
||||
|
||||
private boolean elevationSupported() {
|
||||
return android.os.Build.VERSION.SDK_INT >= 21;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
if (!elevationSupported()) {
|
||||
setMeasuredDimension(getMeasuredWidth() + mShadowRadius * 2, getMeasuredHeight()
|
||||
+ mShadowRadius * 2);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAnimationListener(Animation.AnimationListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart() {
|
||||
super.onAnimationStart();
|
||||
if (mListener != null) {
|
||||
mListener.onAnimationStart(getAnimation());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd() {
|
||||
super.onAnimationEnd();
|
||||
if (mListener != null) {
|
||||
mListener.onAnimationEnd(getAnimation());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the background color of the circle image view.
|
||||
*
|
||||
* @param colorRes Id of a color resource.
|
||||
*/
|
||||
public void setBackgroundColorRes(int colorRes) {
|
||||
setBackgroundColor(ContextCompat.getColor(getContext(), colorRes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBackgroundColor(int color) {
|
||||
if (getBackground() instanceof ShapeDrawable) {
|
||||
((ShapeDrawable) getBackground()).getPaint().setColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
private class OvalShadow extends OvalShape {
|
||||
private RadialGradient mRadialGradient;
|
||||
private Paint mShadowPaint;
|
||||
|
||||
OvalShadow(int shadowRadius) {
|
||||
super();
|
||||
mShadowPaint = new Paint();
|
||||
mShadowRadius = shadowRadius;
|
||||
updateRadialGradient((int) rect().width());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResize(float width, float height) {
|
||||
super.onResize(width, height);
|
||||
updateRadialGradient((int) width);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas, Paint paint) {
|
||||
final int viewWidth = CircleImageView.this.getWidth();
|
||||
final int viewHeight = CircleImageView.this.getHeight();
|
||||
canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2, mShadowPaint);
|
||||
canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2 - mShadowRadius, paint);
|
||||
}
|
||||
|
||||
private void updateRadialGradient(int diameter) {
|
||||
mRadialGradient = new RadialGradient(diameter / 2, diameter / 2,
|
||||
mShadowRadius, new int[] { FILL_SHADOW_COLOR, Color.TRANSPARENT },
|
||||
null, Shader.TileMode.CLAMP);
|
||||
mShadowPaint.setShader(mRadialGradient);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
}
|
||||
1560
app/src/main/java/com/android/apksig/ApkSigner.java
Normal file
1560
app/src/main/java/com/android/apksig/ApkSigner.java
Normal file
File diff suppressed because it is too large
Load Diff
550
app/src/main/java/com/android/apksig/ApkSignerEngine.java
Normal file
550
app/src/main/java/com/android/apksig/ApkSignerEngine.java
Normal 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();
|
||||
}
|
||||
}
|
||||
171
app/src/main/java/com/android/apksig/ApkVerificationIssue.java
Normal file
171
app/src/main/java/com/android/apksig/ApkVerificationIssue.java
Normal 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();
|
||||
}
|
||||
}
|
||||
3244
app/src/main/java/com/android/apksig/ApkVerifier.java
Normal file
3244
app/src/main/java/com/android/apksig/ApkVerifier.java
Normal file
File diff suppressed because it is too large
Load Diff
52
app/src/main/java/com/android/apksig/Constants.java
Normal file
52
app/src/main/java/com/android/apksig/Constants.java
Normal 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";
|
||||
}
|
||||
1844
app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
Normal file
1844
app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
Normal file
File diff suppressed because it is too large
Load Diff
123
app/src/main/java/com/android/apksig/Hints.java
Normal file
123
app/src/main/java/com/android/apksig/Hints.java
Normal 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;
|
||||
}
|
||||
}
|
||||
1076
app/src/main/java/com/android/apksig/SigningCertificateLineage.java
Normal file
1076
app/src/main/java/com/android/apksig/SigningCertificateLineage.java
Normal file
File diff suppressed because it is too large
Load Diff
882
app/src/main/java/com/android/apksig/SourceStampVerifier.java
Normal file
882
app/src/main/java/com/android/apksig/SourceStampVerifier.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a
|
||||
* well-formed ZIP archive, in which case {@link #getCause()} will return a
|
||||
* {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains
|
||||
* multiple ZIP entries with the same name.
|
||||
*/
|
||||
public class ApkFormatException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ApkFormatException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ApkFormatException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that no APK Signing Block was found in an APK.
|
||||
*/
|
||||
public class ApkSigningBlockNotFoundException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ApkSigningBlockNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ApkSigningBlockNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
670
app/src/main/java/com/android/apksig/apk/ApkUtils.java
Normal file
670
app/src/main/java/com/android/apksig/apk/ApkUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
199
app/src/main/java/com/android/apksig/apk/ApkUtilsLite.java
Normal file
199
app/src/main/java/com/android/apksig/apk/ApkUtilsLite.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that there was an issue determining the minimum Android platform version supported by
|
||||
* an APK because the version is specified as a codename, rather than as API Level number, and the
|
||||
* codename is in an unexpected format.
|
||||
*/
|
||||
public class CodenameMinSdkVersionException extends MinSdkVersionException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** Encountered codename. */
|
||||
private final String mCodename;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code MinSdkVersionCodenameException} with the provided message and
|
||||
* codename.
|
||||
*/
|
||||
public CodenameMinSdkVersionException(String message, String codename) {
|
||||
super(message);
|
||||
mCodename = codename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the codename.
|
||||
*/
|
||||
public String getCodename() {
|
||||
return mCodename;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that there was an issue determining the minimum Android platform version supported by
|
||||
* an APK.
|
||||
*/
|
||||
public class MinSdkVersionException extends ApkFormatException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code MinSdkVersionException} with the provided message.
|
||||
*/
|
||||
public MinSdkVersionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code MinSdkVersionException} with the provided message and cause.
|
||||
*/
|
||||
public MinSdkVersionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,869 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}.
|
||||
*
|
||||
* <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
|
||||
* {@link #getEventType()} and {@link #next()} methods. Additional information about the current
|
||||
* event can be obtained via an assortment of getters, for example, {@link #getName()} or
|
||||
* {@link #getAttributeNameResourceId(int)}.
|
||||
*/
|
||||
public class AndroidBinXmlParser {
|
||||
|
||||
/** Event: start of document. */
|
||||
public static final int EVENT_START_DOCUMENT = 1;
|
||||
|
||||
/** Event: end of document. */
|
||||
public static final int EVENT_END_DOCUMENT = 2;
|
||||
|
||||
/** Event: start of an element. */
|
||||
public static final int EVENT_START_ELEMENT = 3;
|
||||
|
||||
/** Event: end of an document. */
|
||||
public static final int EVENT_END_ELEMENT = 4;
|
||||
|
||||
/** Attribute value type is not supported by this parser. */
|
||||
public static final int VALUE_TYPE_UNSUPPORTED = 0;
|
||||
|
||||
/** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
|
||||
public static final int VALUE_TYPE_STRING = 1;
|
||||
|
||||
/** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
|
||||
public static final int VALUE_TYPE_INT = 2;
|
||||
|
||||
/**
|
||||
* Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
|
||||
*/
|
||||
public static final int VALUE_TYPE_REFERENCE = 3;
|
||||
|
||||
/** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
|
||||
public static final int VALUE_TYPE_BOOLEAN = 4;
|
||||
|
||||
private static final long NO_NAMESPACE = 0xffffffffL;
|
||||
|
||||
private final ByteBuffer mXml;
|
||||
|
||||
private StringPool mStringPool;
|
||||
private ResourceMap mResourceMap;
|
||||
private int mDepth;
|
||||
private int mCurrentEvent = EVENT_START_DOCUMENT;
|
||||
|
||||
private String mCurrentElementName;
|
||||
private String mCurrentElementNamespace;
|
||||
private int mCurrentElementAttributeCount;
|
||||
private List<Attribute> mCurrentElementAttributes;
|
||||
private ByteBuffer mCurrentElementAttributesContents;
|
||||
private int mCurrentElementAttrSizeBytes;
|
||||
|
||||
/**
|
||||
* Constructs a new parser for the provided document.
|
||||
*/
|
||||
public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
|
||||
xml.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
Chunk resXmlChunk = null;
|
||||
while (xml.hasRemaining()) {
|
||||
Chunk chunk = Chunk.get(xml);
|
||||
if (chunk == null) {
|
||||
break;
|
||||
}
|
||||
if (chunk.getType() == Chunk.TYPE_RES_XML) {
|
||||
resXmlChunk = chunk;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (resXmlChunk == null) {
|
||||
throw new XmlParserException("No XML chunk in file");
|
||||
}
|
||||
mXml = resXmlChunk.getContents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the depth of the current element. Outside of the root of the document the depth is
|
||||
* {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
|
||||
* is decremented by {@code 1} after each {@code end element} event.
|
||||
*/
|
||||
public int getDepth() {
|
||||
return mDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the current event. See {@code EVENT_...} constants.
|
||||
*/
|
||||
public int getEventType() {
|
||||
return mCurrentEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local name of the current element or {@code null} if the current event does not
|
||||
* pertain to an element.
|
||||
*/
|
||||
public String getName() {
|
||||
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||
return null;
|
||||
}
|
||||
return mCurrentElementName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace of the current element or {@code null} if the current event does not
|
||||
* pertain to an element. Returns an empty string if the element is not associated with a
|
||||
* namespace.
|
||||
*/
|
||||
public String getNamespace() {
|
||||
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||
return null;
|
||||
}
|
||||
return mCurrentElementNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of attributes of the element associated with the current event or
|
||||
* {@code -1} if no element is associated with the current event.
|
||||
*/
|
||||
public int getAttributeCount() {
|
||||
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return mCurrentElementAttributeCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource ID corresponding to the name of the specified attribute of the current
|
||||
* element or {@code 0} if the name is not associated with a resource ID.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public int getAttributeNameResourceId(int index) throws XmlParserException {
|
||||
return getAttribute(index).getNameResourceId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the specified attribute of the current element.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public String getAttributeName(int index) throws XmlParserException {
|
||||
return getAttribute(index).getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the specified attribute of the current element or an empty string if
|
||||
* the attribute is not associated with a namespace.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public String getAttributeNamespace(int index) throws XmlParserException {
|
||||
return getAttribute(index).getNamespace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value type of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public int getAttributeValueType(int index) throws XmlParserException {
|
||||
int type = getAttribute(index).getValueType();
|
||||
switch (type) {
|
||||
case Attribute.TYPE_STRING:
|
||||
return VALUE_TYPE_STRING;
|
||||
case Attribute.TYPE_INT_DEC:
|
||||
case Attribute.TYPE_INT_HEX:
|
||||
return VALUE_TYPE_INT;
|
||||
case Attribute.TYPE_REFERENCE:
|
||||
return VALUE_TYPE_REFERENCE;
|
||||
case Attribute.TYPE_INT_BOOLEAN:
|
||||
return VALUE_TYPE_BOOLEAN;
|
||||
default:
|
||||
return VALUE_TYPE_UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event.
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public int getAttributeIntValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getIntValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the boolean value of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event.
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public boolean getAttributeBooleanValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getBooleanValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event.
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public String getAttributeStringValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getStringValue();
|
||||
}
|
||||
|
||||
private Attribute getAttribute(int index) {
|
||||
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||
throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
|
||||
}
|
||||
if (index < 0) {
|
||||
throw new IndexOutOfBoundsException("index must be >= 0");
|
||||
}
|
||||
if (index >= mCurrentElementAttributeCount) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"index must be <= attr count (" + mCurrentElementAttributeCount + ")");
|
||||
}
|
||||
parseCurrentElementAttributesIfNotParsed();
|
||||
return mCurrentElementAttributes.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
|
||||
*/
|
||||
public int next() throws XmlParserException {
|
||||
// Decrement depth if the previous event was "end element".
|
||||
if (mCurrentEvent == EVENT_END_ELEMENT) {
|
||||
mDepth--;
|
||||
}
|
||||
|
||||
// Read events from document, ignoring events that we don't report to caller. Stop at the
|
||||
// earliest event which we report to caller.
|
||||
while (mXml.hasRemaining()) {
|
||||
Chunk chunk = Chunk.get(mXml);
|
||||
if (chunk == null) {
|
||||
break;
|
||||
}
|
||||
switch (chunk.getType()) {
|
||||
case Chunk.TYPE_STRING_POOL:
|
||||
if (mStringPool != null) {
|
||||
throw new XmlParserException("Multiple string pools not supported");
|
||||
}
|
||||
mStringPool = new StringPool(chunk);
|
||||
break;
|
||||
|
||||
case Chunk.RES_XML_TYPE_START_ELEMENT:
|
||||
{
|
||||
if (mStringPool == null) {
|
||||
throw new XmlParserException(
|
||||
"Named element encountered before string pool");
|
||||
}
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (contents.remaining() < 20) {
|
||||
throw new XmlParserException(
|
||||
"Start element chunk too short. Need at least 20 bytes. Available: "
|
||||
+ contents.remaining() + " bytes");
|
||||
}
|
||||
long nsId = getUnsignedInt32(contents);
|
||||
long nameId = getUnsignedInt32(contents);
|
||||
int attrStartOffset = getUnsignedInt16(contents);
|
||||
int attrSizeBytes = getUnsignedInt16(contents);
|
||||
int attrCount = getUnsignedInt16(contents);
|
||||
long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
|
||||
contents.position(0);
|
||||
if (attrStartOffset > contents.remaining()) {
|
||||
throw new XmlParserException(
|
||||
"Attributes start offset out of bounds: " + attrStartOffset
|
||||
+ ", max: " + contents.remaining());
|
||||
}
|
||||
if (attrEndOffset > contents.remaining()) {
|
||||
throw new XmlParserException(
|
||||
"Attributes end offset out of bounds: " + attrEndOffset
|
||||
+ ", max: " + contents.remaining());
|
||||
}
|
||||
|
||||
mCurrentElementName = mStringPool.getString(nameId);
|
||||
mCurrentElementNamespace =
|
||||
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||
mCurrentElementAttributeCount = attrCount;
|
||||
mCurrentElementAttributes = null;
|
||||
mCurrentElementAttrSizeBytes = attrSizeBytes;
|
||||
mCurrentElementAttributesContents =
|
||||
sliceFromTo(contents, attrStartOffset, attrEndOffset);
|
||||
|
||||
mDepth++;
|
||||
mCurrentEvent = EVENT_START_ELEMENT;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
|
||||
case Chunk.RES_XML_TYPE_END_ELEMENT:
|
||||
{
|
||||
if (mStringPool == null) {
|
||||
throw new XmlParserException(
|
||||
"Named element encountered before string pool");
|
||||
}
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (contents.remaining() < 8) {
|
||||
throw new XmlParserException(
|
||||
"End element chunk too short. Need at least 8 bytes. Available: "
|
||||
+ contents.remaining() + " bytes");
|
||||
}
|
||||
long nsId = getUnsignedInt32(contents);
|
||||
long nameId = getUnsignedInt32(contents);
|
||||
mCurrentElementName = mStringPool.getString(nameId);
|
||||
mCurrentElementNamespace =
|
||||
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||
mCurrentEvent = EVENT_END_ELEMENT;
|
||||
mCurrentElementAttributes = null;
|
||||
mCurrentElementAttributesContents = null;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
case Chunk.RES_XML_TYPE_RESOURCE_MAP:
|
||||
if (mResourceMap != null) {
|
||||
throw new XmlParserException("Multiple resource maps not supported");
|
||||
}
|
||||
mResourceMap = new ResourceMap(chunk);
|
||||
break;
|
||||
default:
|
||||
// Unknown chunk type -- ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mCurrentEvent = EVENT_END_DOCUMENT;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
|
||||
private void parseCurrentElementAttributesIfNotParsed() {
|
||||
if (mCurrentElementAttributes != null) {
|
||||
return;
|
||||
}
|
||||
mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
|
||||
for (int i = 0; i < mCurrentElementAttributeCount; i++) {
|
||||
int startPosition = i * mCurrentElementAttrSizeBytes;
|
||||
ByteBuffer attr =
|
||||
sliceFromTo(
|
||||
mCurrentElementAttributesContents,
|
||||
startPosition,
|
||||
startPosition + mCurrentElementAttrSizeBytes);
|
||||
long nsId = getUnsignedInt32(attr);
|
||||
long nameId = getUnsignedInt32(attr);
|
||||
attr.position(attr.position() + 7); // skip ignored fields
|
||||
int valueType = getUnsignedInt8(attr);
|
||||
long valueData = getUnsignedInt32(attr);
|
||||
mCurrentElementAttributes.add(
|
||||
new Attribute(
|
||||
nsId,
|
||||
nameId,
|
||||
valueType,
|
||||
(int) valueData,
|
||||
mStringPool,
|
||||
mResourceMap));
|
||||
}
|
||||
}
|
||||
|
||||
private static class Attribute {
|
||||
private static final int TYPE_REFERENCE = 1;
|
||||
private static final int TYPE_STRING = 3;
|
||||
private static final int TYPE_INT_DEC = 0x10;
|
||||
private static final int TYPE_INT_HEX = 0x11;
|
||||
private static final int TYPE_INT_BOOLEAN = 0x12;
|
||||
|
||||
private final long mNsId;
|
||||
private final long mNameId;
|
||||
private final int mValueType;
|
||||
private final int mValueData;
|
||||
private final StringPool mStringPool;
|
||||
private final ResourceMap mResourceMap;
|
||||
|
||||
private Attribute(
|
||||
long nsId,
|
||||
long nameId,
|
||||
int valueType,
|
||||
int valueData,
|
||||
StringPool stringPool,
|
||||
ResourceMap resourceMap) {
|
||||
mNsId = nsId;
|
||||
mNameId = nameId;
|
||||
mValueType = valueType;
|
||||
mValueData = valueData;
|
||||
mStringPool = stringPool;
|
||||
mResourceMap = resourceMap;
|
||||
}
|
||||
|
||||
public int getNameResourceId() {
|
||||
return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
|
||||
}
|
||||
|
||||
public String getName() throws XmlParserException {
|
||||
return mStringPool.getString(mNameId);
|
||||
}
|
||||
|
||||
public String getNamespace() throws XmlParserException {
|
||||
return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
|
||||
}
|
||||
|
||||
public int getValueType() {
|
||||
return mValueType;
|
||||
}
|
||||
|
||||
public int getIntValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_REFERENCE:
|
||||
case TYPE_INT_DEC:
|
||||
case TYPE_INT_HEX:
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return mValueData;
|
||||
default:
|
||||
throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getBooleanValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return mValueData != 0;
|
||||
default:
|
||||
throw new XmlParserException(
|
||||
"Cannot coerce to boolean: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
|
||||
public String getStringValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_STRING:
|
||||
return mStringPool.getString(mValueData & 0xffffffffL);
|
||||
case TYPE_INT_DEC:
|
||||
return Integer.toString(mValueData);
|
||||
case TYPE_INT_HEX:
|
||||
return "0x" + Integer.toHexString(mValueData);
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return Boolean.toString(mValueData != 0);
|
||||
case TYPE_REFERENCE:
|
||||
return "@" + Integer.toHexString(mValueData);
|
||||
default:
|
||||
throw new XmlParserException(
|
||||
"Cannot coerce to string: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
|
||||
* contents.
|
||||
*/
|
||||
private static class Chunk {
|
||||
public static final int TYPE_STRING_POOL = 1;
|
||||
public static final int TYPE_RES_XML = 3;
|
||||
public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
|
||||
public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
|
||||
public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
|
||||
|
||||
static final int HEADER_MIN_SIZE_BYTES = 8;
|
||||
|
||||
private final int mType;
|
||||
private final ByteBuffer mHeader;
|
||||
private final ByteBuffer mContents;
|
||||
|
||||
public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
|
||||
mType = type;
|
||||
mHeader = header;
|
||||
mContents = contents;
|
||||
}
|
||||
|
||||
public ByteBuffer getContents() {
|
||||
ByteBuffer result = mContents.slice();
|
||||
result.order(mContents.order());
|
||||
return result;
|
||||
}
|
||||
|
||||
public ByteBuffer getHeader() {
|
||||
ByteBuffer result = mHeader.slice();
|
||||
result.order(mHeader.order());
|
||||
return result;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes the chunk located at the current position of the input and returns the chunk
|
||||
* or {@code null} if there is no chunk left in the input.
|
||||
*
|
||||
* @throws XmlParserException if the chunk is malformed
|
||||
*/
|
||||
public static Chunk get(ByteBuffer input) throws XmlParserException {
|
||||
if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
|
||||
// Android ignores the last chunk if its header is too big to fit into the file
|
||||
input.position(input.limit());
|
||||
return null;
|
||||
}
|
||||
|
||||
int originalPosition = input.position();
|
||||
int type = getUnsignedInt16(input);
|
||||
int headerSize = getUnsignedInt16(input);
|
||||
long chunkSize = getUnsignedInt32(input);
|
||||
long chunkRemaining = chunkSize - 8;
|
||||
if (chunkRemaining > input.remaining()) {
|
||||
// Android ignores the last chunk if it's too big to fit into the file
|
||||
input.position(input.limit());
|
||||
return null;
|
||||
}
|
||||
if (headerSize < HEADER_MIN_SIZE_BYTES) {
|
||||
throw new XmlParserException(
|
||||
"Malformed chunk: header too short: " + headerSize + " bytes");
|
||||
} else if (headerSize > chunkSize) {
|
||||
throw new XmlParserException(
|
||||
"Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
|
||||
+ chunkSize + " bytes");
|
||||
}
|
||||
int contentStartPosition = originalPosition + headerSize;
|
||||
long chunkEndPosition = originalPosition + chunkSize;
|
||||
Chunk chunk =
|
||||
new Chunk(
|
||||
type,
|
||||
sliceFromTo(input, originalPosition, contentStartPosition),
|
||||
sliceFromTo(input, contentStartPosition, chunkEndPosition));
|
||||
input.position((int) chunkEndPosition);
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
|
||||
*/
|
||||
private static class StringPool {
|
||||
private static final int FLAG_UTF8 = 1 << 8;
|
||||
|
||||
private final ByteBuffer mChunkContents;
|
||||
private final ByteBuffer mStringsSection;
|
||||
private final int mStringCount;
|
||||
private final boolean mUtf8Encoded;
|
||||
private final Map<Integer, String> mCachedStrings = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs a new string pool from the provided chunk.
|
||||
*
|
||||
* @throws XmlParserException if a parsing error occurred
|
||||
*/
|
||||
public StringPool(Chunk chunk) throws XmlParserException {
|
||||
ByteBuffer header = chunk.getHeader();
|
||||
int headerSizeBytes = header.remaining();
|
||||
header.position(Chunk.HEADER_MIN_SIZE_BYTES);
|
||||
if (header.remaining() < 20) {
|
||||
throw new XmlParserException(
|
||||
"XML chunk's header too short. Required at least 20 bytes. Available: "
|
||||
+ header.remaining() + " bytes");
|
||||
}
|
||||
long stringCount = getUnsignedInt32(header);
|
||||
if (stringCount > Integer.MAX_VALUE) {
|
||||
throw new XmlParserException("Too many strings: " + stringCount);
|
||||
}
|
||||
mStringCount = (int) stringCount;
|
||||
long styleCount = getUnsignedInt32(header);
|
||||
if (styleCount > Integer.MAX_VALUE) {
|
||||
throw new XmlParserException("Too many styles: " + styleCount);
|
||||
}
|
||||
long flags = getUnsignedInt32(header);
|
||||
long stringsStartOffset = getUnsignedInt32(header);
|
||||
long stylesStartOffset = getUnsignedInt32(header);
|
||||
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (mStringCount > 0) {
|
||||
int stringsSectionStartOffsetInContents =
|
||||
(int) (stringsStartOffset - headerSizeBytes);
|
||||
int stringsSectionEndOffsetInContents;
|
||||
if (styleCount > 0) {
|
||||
// Styles section follows the strings section
|
||||
if (stylesStartOffset < stringsStartOffset) {
|
||||
throw new XmlParserException(
|
||||
"Styles offset (" + stylesStartOffset + ") < strings offset ("
|
||||
+ stringsStartOffset + ")");
|
||||
}
|
||||
stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
|
||||
} else {
|
||||
stringsSectionEndOffsetInContents = contents.remaining();
|
||||
}
|
||||
mStringsSection =
|
||||
sliceFromTo(
|
||||
contents,
|
||||
stringsSectionStartOffsetInContents,
|
||||
stringsSectionEndOffsetInContents);
|
||||
} else {
|
||||
mStringsSection = ByteBuffer.allocate(0);
|
||||
}
|
||||
|
||||
mUtf8Encoded = (flags & FLAG_UTF8) != 0;
|
||||
mChunkContents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string located at the specified {@code 0}-based index in this pool.
|
||||
*
|
||||
* @throws XmlParserException if the string does not exist or cannot be decoded
|
||||
*/
|
||||
public String getString(long index) throws XmlParserException {
|
||||
if (index < 0) {
|
||||
throw new XmlParserException("Unsuported string index: " + index);
|
||||
} else if (index >= mStringCount) {
|
||||
throw new XmlParserException(
|
||||
"Unsuported string index: " + index + ", max: " + (mStringCount - 1));
|
||||
}
|
||||
|
||||
int idx = (int) index;
|
||||
String result = mCachedStrings.get(idx);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
|
||||
if (offsetInStringsSection >= mStringsSection.capacity()) {
|
||||
throw new XmlParserException(
|
||||
"Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
|
||||
+ ", max: " + (mStringsSection.capacity() - 1));
|
||||
}
|
||||
mStringsSection.position((int) offsetInStringsSection);
|
||||
result =
|
||||
(mUtf8Encoded)
|
||||
? getLengthPrefixedUtf8EncodedString(mStringsSection)
|
||||
: getLengthPrefixedUtf16EncodedString(mStringsSection);
|
||||
mCachedStrings.put(idx, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
|
||||
throws XmlParserException {
|
||||
// If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
|
||||
// Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
|
||||
// of supported values is 0 to 0x7fffffff inclusive.
|
||||
int lengthChars = getUnsignedInt16(encoded);
|
||||
if ((lengthChars & 0x8000) != 0) {
|
||||
lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
|
||||
}
|
||||
if (lengthChars > Integer.MAX_VALUE / 2) {
|
||||
throw new XmlParserException("String too long: " + lengthChars + " uint16s");
|
||||
}
|
||||
int lengthBytes = lengthChars * 2;
|
||||
|
||||
byte[] arr;
|
||||
int arrOffset;
|
||||
if (encoded.hasArray()) {
|
||||
arr = encoded.array();
|
||||
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||
encoded.position(encoded.position() + lengthBytes);
|
||||
} else {
|
||||
arr = new byte[lengthBytes];
|
||||
arrOffset = 0;
|
||||
encoded.get(arr);
|
||||
}
|
||||
// Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
|
||||
// array of bytes is NULL terminated.
|
||||
if ((arr[arrOffset + lengthBytes] != 0)
|
||||
|| (arr[arrOffset + lengthBytes + 1] != 0)) {
|
||||
throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
|
||||
}
|
||||
try {
|
||||
return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-16LE character encoding not supported", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
|
||||
throws XmlParserException {
|
||||
// If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
|
||||
// it is stored as a big-endian uint16 with highest bit set. Thus, the range of
|
||||
// supported values is 0 to 0x7fff inclusive.
|
||||
|
||||
// Skip UTF-16 encoded length (in uint16s)
|
||||
int lengthBytes = getUnsignedInt8(encoded);
|
||||
if ((lengthBytes & 0x80) != 0) {
|
||||
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||
}
|
||||
|
||||
// Read UTF-8 encoded length (in bytes)
|
||||
lengthBytes = getUnsignedInt8(encoded);
|
||||
if ((lengthBytes & 0x80) != 0) {
|
||||
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||
}
|
||||
|
||||
byte[] arr;
|
||||
int arrOffset;
|
||||
if (encoded.hasArray()) {
|
||||
arr = encoded.array();
|
||||
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||
encoded.position(encoded.position() + lengthBytes);
|
||||
} else {
|
||||
arr = new byte[lengthBytes];
|
||||
arrOffset = 0;
|
||||
encoded.get(arr);
|
||||
}
|
||||
// Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
|
||||
// of bytes is NULL terminated.
|
||||
if (arr[arrOffset + lengthBytes] != 0) {
|
||||
throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
|
||||
}
|
||||
try {
|
||||
return new String(arr, arrOffset, lengthBytes, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 character encoding not supported", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
|
||||
* map.
|
||||
*/
|
||||
private static class ResourceMap {
|
||||
private final ByteBuffer mChunkContents;
|
||||
private final int mEntryCount;
|
||||
|
||||
/**
|
||||
* Constructs a new resource map from the provided chunk.
|
||||
*
|
||||
* @throws XmlParserException if a parsing error occurred
|
||||
*/
|
||||
public ResourceMap(Chunk chunk) throws XmlParserException {
|
||||
mChunkContents = chunk.getContents().slice();
|
||||
mChunkContents.order(chunk.getContents().order());
|
||||
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||
mEntryCount = mChunkContents.remaining() / 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource ID located at the specified {@code 0}-based index in this pool or
|
||||
* {@code 0} if the index is out of range.
|
||||
*/
|
||||
public int getResourceId(long index) {
|
||||
if ((index < 0) || (index >= mEntryCount)) {
|
||||
return 0;
|
||||
}
|
||||
int idx = (int) index;
|
||||
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||
return mChunkContents.getInt(idx * 4);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||
* buffer's byte order.
|
||||
*/
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
return sliceFromTo(source, (int) start, (int) end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||
* buffer's byte order.
|
||||
*/
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int originalPosition = source.position();
|
||||
try {
|
||||
source.position(0);
|
||||
source.limit(end);
|
||||
source.position(start);
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
return result;
|
||||
} finally {
|
||||
source.position(0);
|
||||
source.limit(originalLimit);
|
||||
source.position(originalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getUnsignedInt8(ByteBuffer buffer) {
|
||||
return buffer.get() & 0xff;
|
||||
}
|
||||
|
||||
private static int getUnsignedInt16(ByteBuffer buffer) {
|
||||
return buffer.getShort() & 0xffff;
|
||||
}
|
||||
|
||||
private static long getUnsignedInt32(ByteBuffer buffer) {
|
||||
return buffer.getInt() & 0xffffffffL;
|
||||
}
|
||||
|
||||
private static long getUnsignedInt32(ByteBuffer buffer, int position) {
|
||||
return buffer.getInt(position) & 0xffffffffL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred while parsing a document.
|
||||
*/
|
||||
public static class XmlParserException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public XmlParserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public XmlParserException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.ApkVerificationIssue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base implementation of an APK signature verification result.
|
||||
*/
|
||||
public class ApkSigResult {
|
||||
public final int signatureSchemeVersion;
|
||||
|
||||
/** Whether the APK's Signature Scheme signature verifies. */
|
||||
public boolean verified;
|
||||
|
||||
public final List<ApkSignerInfo> mSigners = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
|
||||
public ApkSigResult(int signatureSchemeVersion) {
|
||||
this.signatureSchemeVersion = signatureSchemeVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this result encountered errors during verification.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
if (!mErrors.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (!mSigners.isEmpty()) {
|
||||
for (ApkSignerInfo signer : mSigners) {
|
||||
if (signer.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this result encountered warnings during verification.
|
||||
*/
|
||||
public boolean containsWarnings() {
|
||||
if (!mWarnings.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (!mSigners.isEmpty()) {
|
||||
for (ApkSignerInfo signer : mSigners) {
|
||||
if (signer.containsWarnings()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code
|
||||
* issueId} and {@code params}.
|
||||
*/
|
||||
public void addError(int issueId, Object... parameters) {
|
||||
mErrors.add(new ApkVerificationIssue(issueId, parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code
|
||||
* issueId} and {@code params}.
|
||||
*/
|
||||
public void addWarning(int issueId, Object... parameters) {
|
||||
mWarnings.add(new ApkVerificationIssue(issueId, parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errors encountered during verification.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warnings encountered during verification.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,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
@ -0,0 +1,393 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
|
||||
import com.android.apksig.apk.ApkUtilsLite;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.zip.ZipSections;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the
|
||||
* utility functionality.
|
||||
*/
|
||||
public class ApkSigningBlockUtilsLite {
|
||||
private ApkSigningBlockUtilsLite() {}
|
||||
|
||||
private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
|
||||
/**
|
||||
* Returns the APK Signature Scheme block contained in the provided APK file for the given ID
|
||||
* and the additional information relevant for verifying the block against the file.
|
||||
*
|
||||
* @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
|
||||
* identifying the appropriate block to find, e.g. the APK Signature Scheme v2
|
||||
* block ID.
|
||||
*
|
||||
* @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
|
||||
* @throws IOException if an I/O error occurs while reading the APK
|
||||
*/
|
||||
public static SignatureInfo findSignature(
|
||||
DataSource apk, ZipSections zipSections, int blockId)
|
||||
throws IOException, SignatureNotFoundException {
|
||||
// Find the APK Signing Block.
|
||||
DataSource apkSigningBlock;
|
||||
long apkSigningBlockOffset;
|
||||
try {
|
||||
ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo =
|
||||
ApkUtilsLite.findApkSigningBlock(apk, zipSections);
|
||||
apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
|
||||
apkSigningBlock = apkSigningBlockInfo.getContents();
|
||||
} catch (ApkSigningBlockNotFoundException e) {
|
||||
throw new SignatureNotFoundException(e.getMessage(), e);
|
||||
}
|
||||
ByteBuffer apkSigningBlockBuf =
|
||||
apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
|
||||
apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// Find the APK Signature Scheme Block inside the APK Signing Block.
|
||||
ByteBuffer apkSignatureSchemeBlock =
|
||||
findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId);
|
||||
return new SignatureInfo(
|
||||
apkSignatureSchemeBlock,
|
||||
apkSigningBlockOffset,
|
||||
zipSections.getZipCentralDirectoryOffset(),
|
||||
zipSections.getZipEndOfCentralDirectoryOffset(),
|
||||
zipSections.getZipEndOfCentralDirectory());
|
||||
}
|
||||
|
||||
public static ByteBuffer findApkSignatureSchemeBlock(
|
||||
ByteBuffer apkSigningBlock,
|
||||
int blockId) throws SignatureNotFoundException {
|
||||
checkByteOrderLittleEndian(apkSigningBlock);
|
||||
// FORMAT:
|
||||
// OFFSET DATA TYPE DESCRIPTION
|
||||
// * @+0 bytes uint64: size in bytes (excluding this field)
|
||||
// * @+8 bytes pairs
|
||||
// * @-24 bytes uint64: size in bytes (same as the one above)
|
||||
// * @-16 bytes uint128: magic
|
||||
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
|
||||
|
||||
int entryCount = 0;
|
||||
while (pairs.hasRemaining()) {
|
||||
entryCount++;
|
||||
if (pairs.remaining() < 8) {
|
||||
throw new SignatureNotFoundException(
|
||||
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
|
||||
}
|
||||
long lenLong = pairs.getLong();
|
||||
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block entry #" + entryCount
|
||||
+ " size out of range: " + lenLong);
|
||||
}
|
||||
int len = (int) lenLong;
|
||||
int nextEntryPos = pairs.position() + len;
|
||||
if (len > pairs.remaining()) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block entry #" + entryCount + " size out of range: " + len
|
||||
+ ", available: " + pairs.remaining());
|
||||
}
|
||||
int id = pairs.getInt();
|
||||
if (id == blockId) {
|
||||
return getByteBuffer(pairs, len - 4);
|
||||
}
|
||||
pairs.position(nextEntryPos);
|
||||
}
|
||||
|
||||
throw new SignatureNotFoundException(
|
||||
"No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
|
||||
}
|
||||
|
||||
public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
|
||||
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
||||
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of signatures which are expected to be verified by at least one Android
|
||||
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
|
||||
* guaranteed to contain at least one signature.
|
||||
*
|
||||
* <p>Each Android platform version typically verifies exactly one signature from the provided
|
||||
* {@code signatures} set. This method returns the set of these signatures collected over all
|
||||
* requested platform versions. As a result, the result may contain more than one signature.
|
||||
*
|
||||
* @throws NoApkSupportedSignaturesException if no supported signatures were
|
||||
* found for an Android platform version in the range.
|
||||
*/
|
||||
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
|
||||
List<T> signatures, int minSdkVersion, int maxSdkVersion)
|
||||
throws NoApkSupportedSignaturesException {
|
||||
return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of signatures which are expected to be verified by at least one Android
|
||||
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
|
||||
* guaranteed to contain at least one signature.
|
||||
*
|
||||
* <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
|
||||
* signature within the signing block using the standard JCA.
|
||||
*
|
||||
* <p>Each Android platform version typically verifies exactly one signature from the provided
|
||||
* {@code signatures} set. This method returns the set of these signatures collected over all
|
||||
* requested platform versions. As a result, the result may contain more than one signature.
|
||||
*
|
||||
* @throws NoApkSupportedSignaturesException if no supported signatures were
|
||||
* found for an Android platform version in the range.
|
||||
*/
|
||||
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
|
||||
List<T> signatures, int minSdkVersion, int maxSdkVersion,
|
||||
boolean onlyRequireJcaSupport) throws
|
||||
NoApkSupportedSignaturesException {
|
||||
// Pick the signature with the strongest algorithm at all required SDK versions, to mimic
|
||||
// Android's behavior on those versions.
|
||||
//
|
||||
// Here we assume that, once introduced, a signature algorithm continues to be supported in
|
||||
// all future Android versions. We also assume that the better-than relationship between
|
||||
// algorithms is exactly the same on all Android platform versions (except that older
|
||||
// platforms might support fewer algorithms). If these assumption are no longer true, the
|
||||
// logic here will need to change accordingly.
|
||||
Map<Integer, T>
|
||||
bestSigAlgorithmOnSdkVersion = new HashMap<>();
|
||||
int minProvidedSignaturesVersion = Integer.MAX_VALUE;
|
||||
for (T sig : signatures) {
|
||||
SignatureAlgorithm sigAlgorithm = sig.algorithm;
|
||||
int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
|
||||
: sigAlgorithm.getMinSdkVersion();
|
||||
if (sigMinSdkVersion > maxSdkVersion) {
|
||||
continue;
|
||||
}
|
||||
if (sigMinSdkVersion < minProvidedSignaturesVersion) {
|
||||
minProvidedSignaturesVersion = sigMinSdkVersion;
|
||||
}
|
||||
|
||||
T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
|
||||
if ((candidate == null)
|
||||
|| (compareSignatureAlgorithm(
|
||||
sigAlgorithm, candidate.algorithm) > 0)) {
|
||||
bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
|
||||
}
|
||||
}
|
||||
|
||||
// Must have some supported signature algorithms for minSdkVersion.
|
||||
if (minSdkVersion < minProvidedSignaturesVersion) {
|
||||
throw new NoApkSupportedSignaturesException(
|
||||
"Minimum provided signature version " + minProvidedSignaturesVersion +
|
||||
" > minSdkVersion " + minSdkVersion);
|
||||
}
|
||||
if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
|
||||
throw new NoApkSupportedSignaturesException("No supported signature");
|
||||
}
|
||||
List<T> signaturesToVerify =
|
||||
new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
|
||||
Collections.sort(
|
||||
signaturesToVerify,
|
||||
(sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
|
||||
return signaturesToVerify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
|
||||
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
|
||||
*/
|
||||
public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
|
||||
ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
|
||||
ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
|
||||
return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
|
||||
* if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
|
||||
*/
|
||||
private static int compareContentDigestAlgorithm(
|
||||
ContentDigestAlgorithm alg1,
|
||||
ContentDigestAlgorithm alg2) {
|
||||
switch (alg1) {
|
||||
case CHUNKED_SHA256:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
return 0;
|
||||
case CHUNKED_SHA512:
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return -1;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
case CHUNKED_SHA512:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return 1;
|
||||
case CHUNKED_SHA512:
|
||||
return 0;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
return 1;
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return 0;
|
||||
case CHUNKED_SHA512:
|
||||
return -1;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg1: " + alg1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||
* buffer's byte order.
|
||||
*/
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int originalPosition = source.position();
|
||||
try {
|
||||
source.position(0);
|
||||
source.limit(end);
|
||||
source.position(start);
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
return result;
|
||||
} finally {
|
||||
source.position(0);
|
||||
source.limit(originalLimit);
|
||||
source.position(originalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
|
||||
* position of this buffer.
|
||||
*
|
||||
* <p>This method reads the next {@code size} bytes at this buffer's current position,
|
||||
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
|
||||
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
|
||||
* {@code size}.
|
||||
*/
|
||||
private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
|
||||
if (size < 0) {
|
||||
throw new IllegalArgumentException("size: " + size);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int position = source.position();
|
||||
int limit = position + size;
|
||||
if ((limit < position) || (limit > originalLimit)) {
|
||||
throw new BufferUnderflowException();
|
||||
}
|
||||
source.limit(limit);
|
||||
try {
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
source.position(limit);
|
||||
return result;
|
||||
} finally {
|
||||
source.limit(originalLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public static String toHex(byte[] value) {
|
||||
StringBuilder sb = new StringBuilder(value.length * 2);
|
||||
int len = value.length;
|
||||
for (int i = 0; i < len; i++) {
|
||||
int hi = (value[i] & 0xff) >>> 4;
|
||||
int lo = value[i] & 0x0f;
|
||||
sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
|
||||
if (source.remaining() < 4) {
|
||||
throw new ApkFormatException(
|
||||
"Remaining buffer too short to contain length of length-prefixed field"
|
||||
+ ". Remaining: " + source.remaining());
|
||||
}
|
||||
int len = source.getInt();
|
||||
if (len < 0) {
|
||||
throw new IllegalArgumentException("Negative length");
|
||||
} else if (len > source.remaining()) {
|
||||
throw new ApkFormatException(
|
||||
"Length-prefixed field longer than remaining buffer"
|
||||
+ ". Field length: " + len + ", remaining: " + source.remaining());
|
||||
}
|
||||
return getByteBuffer(source, len);
|
||||
}
|
||||
|
||||
public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
|
||||
int len = buf.getInt();
|
||||
if (len < 0) {
|
||||
throw new ApkFormatException("Negative length");
|
||||
} else if (len > buf.remaining()) {
|
||||
throw new ApkFormatException(
|
||||
"Underflow while reading length-prefixed value. Length: " + len
|
||||
+ ", available: " + buf.remaining());
|
||||
}
|
||||
byte[] result = new byte[len];
|
||||
buf.get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
List<Pair<Integer, byte[]>> sequence) {
|
||||
int resultSize = 0;
|
||||
for (Pair<Integer, byte[]> element : sequence) {
|
||||
resultSize += 12 + element.getSecond().length;
|
||||
}
|
||||
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for (Pair<Integer, byte[]> element : sequence) {
|
||||
byte[] second = element.getSecond();
|
||||
result.putInt(8 + second.length);
|
||||
result.putInt(element.getFirst());
|
||||
result.putInt(second.length);
|
||||
result.put(second);
|
||||
}
|
||||
return result.array();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/**
|
||||
* Base implementation of a supported signature for an APK.
|
||||
*/
|
||||
public class ApkSupportedSignature {
|
||||
public final SignatureAlgorithm algorithm;
|
||||
public final byte[] signature;
|
||||
|
||||
/**
|
||||
* Constructs a new supported signature using the provided {@code algorithm} and {@code
|
||||
* signature} bytes.
|
||||
*/
|
||||
public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
|
||||
this.algorithm = algorithm;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/** APK Signature Scheme v2 content digest algorithm. */
|
||||
public enum ContentDigestAlgorithm {
|
||||
/** SHA2-256 over 1 MB chunks. */
|
||||
CHUNKED_SHA256(1, "SHA-256", 256 / 8),
|
||||
|
||||
/** SHA2-512 over 1 MB chunks. */
|
||||
CHUNKED_SHA512(2, "SHA-512", 512 / 8),
|
||||
|
||||
/** SHA2-256 over 4 KB chunks for APK verity. */
|
||||
VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
|
||||
|
||||
/** Non-chunk SHA2-256. */
|
||||
SHA256(4, "SHA-256", 256 / 8);
|
||||
|
||||
private final int mId;
|
||||
private final String mJcaMessageDigestAlgorithm;
|
||||
private final int mChunkDigestOutputSizeBytes;
|
||||
|
||||
private ContentDigestAlgorithm(
|
||||
int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
|
||||
mId = id;
|
||||
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
|
||||
mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
|
||||
}
|
||||
|
||||
/** Returns the ID of the digest algorithm used on the APK. */
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
|
||||
* chunks by this content digest algorithm.
|
||||
*/
|
||||
String getJcaMessageDigestAlgorithm() {
|
||||
return mJcaMessageDigestAlgorithm;
|
||||
}
|
||||
|
||||
/** Returns the size (in bytes) of the digest of a chunk of content. */
|
||||
int getChunkDigestOutputSizeBytes() {
|
||||
return mChunkDigestOutputSizeBytes;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/**
|
||||
* Base exception that is thrown when there are no signatures that support the full range of
|
||||
* requested platform versions.
|
||||
*/
|
||||
public class NoApkSupportedSignaturesException extends Exception {
|
||||
public NoApkSupportedSignaturesException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.PSSParameterSpec;
|
||||
|
||||
/**
|
||||
* APK Signing Block signature algorithm.
|
||||
*/
|
||||
public enum SignatureAlgorithm {
|
||||
// TODO reserve the 0x0000 ID to mean null
|
||||
/**
|
||||
* RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
|
||||
* digested using SHA2-256 in 1 MB chunks.
|
||||
*/
|
||||
RSA_PSS_WITH_SHA256(
|
||||
0x0101,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"RSA",
|
||||
Pair.of("SHA256withRSA/PSS",
|
||||
new PSSParameterSpec(
|
||||
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.M),
|
||||
|
||||
/**
|
||||
* RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
|
||||
* digested using SHA2-512 in 1 MB chunks.
|
||||
*/
|
||||
RSA_PSS_WITH_SHA512(
|
||||
0x0102,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||
"RSA",
|
||||
Pair.of(
|
||||
"SHA512withRSA/PSS",
|
||||
new PSSParameterSpec(
|
||||
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.M),
|
||||
|
||||
/** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||
RSA_PKCS1_V1_5_WITH_SHA256(
|
||||
0x0103,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"RSA",
|
||||
Pair.of("SHA256withRSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
||||
RSA_PKCS1_V1_5_WITH_SHA512(
|
||||
0x0104,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||
"RSA",
|
||||
Pair.of("SHA512withRSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||
ECDSA_WITH_SHA256(
|
||||
0x0201,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"EC",
|
||||
Pair.of("SHA256withECDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.HONEYCOMB),
|
||||
|
||||
/** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
||||
ECDSA_WITH_SHA512(
|
||||
0x0202,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||
"EC",
|
||||
Pair.of("SHA512withECDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.HONEYCOMB),
|
||||
|
||||
/** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||
DSA_WITH_SHA256(
|
||||
0x0301,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"DSA",
|
||||
Pair.of("SHA256withDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/**
|
||||
* DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done
|
||||
* deterministically according to RFC 6979.
|
||||
*/
|
||||
DETDSA_WITH_SHA256(
|
||||
0x0301,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"DSA",
|
||||
Pair.of("SHA256withDetDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/**
|
||||
* RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
|
||||
* the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
|
||||
* in little endian) construct the final digest.
|
||||
*/
|
||||
VERITY_RSA_PKCS1_V1_5_WITH_SHA256(
|
||||
0x0421,
|
||||
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||
"RSA",
|
||||
Pair.of("SHA256withRSA", null),
|
||||
AndroidSdkVersion.P,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/**
|
||||
* ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
|
||||
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
|
||||
* endian) construct the final digest.
|
||||
*/
|
||||
VERITY_ECDSA_WITH_SHA256(
|
||||
0x0423,
|
||||
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||
"EC",
|
||||
Pair.of("SHA256withECDSA", null),
|
||||
AndroidSdkVersion.P,
|
||||
AndroidSdkVersion.HONEYCOMB),
|
||||
|
||||
/**
|
||||
* DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
|
||||
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
|
||||
* endian) construct the final digest.
|
||||
*/
|
||||
VERITY_DSA_WITH_SHA256(
|
||||
0x0425,
|
||||
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||
"DSA",
|
||||
Pair.of("SHA256withDSA", null),
|
||||
AndroidSdkVersion.P,
|
||||
AndroidSdkVersion.INITIAL_RELEASE);
|
||||
|
||||
private final int mId;
|
||||
private final String mJcaKeyAlgorithm;
|
||||
private final ContentDigestAlgorithm mContentDigestAlgorithm;
|
||||
private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
|
||||
private final int mMinSdkVersion;
|
||||
private final int mJcaSigAlgMinSdkVersion;
|
||||
|
||||
SignatureAlgorithm(int id,
|
||||
ContentDigestAlgorithm contentDigestAlgorithm,
|
||||
String jcaKeyAlgorithm,
|
||||
Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
|
||||
int minSdkVersion,
|
||||
int jcaSigAlgMinSdkVersion) {
|
||||
mId = id;
|
||||
mContentDigestAlgorithm = contentDigestAlgorithm;
|
||||
mJcaKeyAlgorithm = jcaKeyAlgorithm;
|
||||
mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
|
||||
*/
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content digest algorithm associated with this signature algorithm.
|
||||
*/
|
||||
public ContentDigestAlgorithm getContentDigestAlgorithm() {
|
||||
return mContentDigestAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
|
||||
*/
|
||||
public String getJcaKeyAlgorithm() {
|
||||
return mJcaKeyAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
|
||||
* (or null if not needed) to parameterize the {@code Signature}.
|
||||
*/
|
||||
public Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
|
||||
return mJcaSignatureAlgAndParams;
|
||||
}
|
||||
|
||||
public int getMinSdkVersion() {
|
||||
return mMinSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum SDK version that supports the JCA signature algorithm.
|
||||
*/
|
||||
public int getJcaSigAlgMinSdkVersion() {
|
||||
return mJcaSigAlgMinSdkVersion;
|
||||
}
|
||||
|
||||
public static SignatureAlgorithm findById(int id) {
|
||||
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
|
||||
if (alg.getId() == id) {
|
||||
return alg;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme block and additional information relevant to verifying the signatures
|
||||
* contained in the block against the file.
|
||||
*/
|
||||
public class SignatureInfo {
|
||||
/** Contents of APK Signature Scheme block. */
|
||||
public final ByteBuffer signatureBlock;
|
||||
|
||||
/** Position of the APK Signing Block in the file. */
|
||||
public final long apkSigningBlockOffset;
|
||||
|
||||
/** Position of the ZIP Central Directory in the file. */
|
||||
public final long centralDirOffset;
|
||||
|
||||
/** Position of the ZIP End of Central Directory (EoCD) in the file. */
|
||||
public final long eocdOffset;
|
||||
|
||||
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
|
||||
public final ByteBuffer eocd;
|
||||
|
||||
public SignatureInfo(
|
||||
ByteBuffer signatureBlock,
|
||||
long apkSigningBlockOffset,
|
||||
long centralDirOffset,
|
||||
long eocdOffset,
|
||||
ByteBuffer eocd) {
|
||||
this.signatureBlock = signatureBlock;
|
||||
this.apkSigningBlockOffset = apkSigningBlockOffset;
|
||||
this.centralDirOffset = centralDirOffset;
|
||||
this.eocdOffset = eocdOffset;
|
||||
this.eocd = eocd;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/**
|
||||
* Base exception that is thrown when the APK is not signed with the requested signature scheme.
|
||||
*/
|
||||
public class SignatureNotFoundException extends Exception {
|
||||
public SignatureNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SignatureNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */
|
||||
public class SourceStampCertificateLineage {
|
||||
|
||||
private final static int FIRST_VERSION = 1;
|
||||
private final static int CURRENT_VERSION = FIRST_VERSION;
|
||||
|
||||
/**
|
||||
* Deserializes the binary representation of a SourceStampCertificateLineage. Also
|
||||
* verifies that the structure is well-formed, e.g. that the signature for each node is from its
|
||||
* parent.
|
||||
*/
|
||||
public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
|
||||
throws IOException {
|
||||
List<SigningCertificateNode> result = new ArrayList<>();
|
||||
int nodeCount = 0;
|
||||
if (inputBytes == null || !inputBytes.hasRemaining()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes);
|
||||
|
||||
CertificateFactory certFactory;
|
||||
try {
|
||||
certFactory = CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||
}
|
||||
|
||||
// FORMAT (little endian):
|
||||
// * uint32: version code
|
||||
// * sequence of length-prefixed (uint32): nodes
|
||||
// * length-prefixed bytes: signed data
|
||||
// * length-prefixed bytes: certificate
|
||||
// * uint32: signature algorithm id
|
||||
// * uint32: flags
|
||||
// * uint32: signature algorithm id (used by to sign next cert in lineage)
|
||||
// * length-prefixed bytes: signature over above signed data
|
||||
|
||||
X509Certificate lastCert = null;
|
||||
int lastSigAlgorithmId = 0;
|
||||
|
||||
try {
|
||||
int version = inputBytes.getInt();
|
||||
if (version != CURRENT_VERSION) {
|
||||
// we only have one version to worry about right now, so just check it
|
||||
throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
|
||||
+ " different than any of which we are aware");
|
||||
}
|
||||
HashSet<X509Certificate> certHistorySet = new HashSet<>();
|
||||
while (inputBytes.hasRemaining()) {
|
||||
nodeCount++;
|
||||
ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
|
||||
ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
|
||||
int flags = nodeBytes.getInt();
|
||||
int sigAlgorithmId = nodeBytes.getInt();
|
||||
SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
|
||||
byte[] signature = readLengthPrefixedByteArray(nodeBytes);
|
||||
|
||||
if (lastCert != null) {
|
||||
// Use previous level cert to verify current level
|
||||
String jcaSignatureAlgorithm =
|
||||
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||
PublicKey publicKey = lastCert.getPublicKey();
|
||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
sig.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
sig.update(signedData);
|
||||
if (!sig.verify(signature)) {
|
||||
throw new SecurityException("Unable to verify signature of certificate #"
|
||||
+ nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
|
||||
+ " SourceStampCertificateLineage object");
|
||||
}
|
||||
}
|
||||
|
||||
signedData.rewind();
|
||||
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
|
||||
int signedSigAlgorithm = signedData.getInt();
|
||||
if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
|
||||
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
|
||||
+ nodeBytes + " when verifying SourceStampCertificateLineage object");
|
||||
}
|
||||
lastCert = (X509Certificate) certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(encodedCert));
|
||||
lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
|
||||
if (certHistorySet.contains(lastCert)) {
|
||||
throw new SecurityException("Encountered duplicate entries in "
|
||||
+ "SigningCertificateLineage at certificate #" + nodeCount + ". All "
|
||||
+ "signing certificates should be unique");
|
||||
}
|
||||
certHistorySet.add(lastCert);
|
||||
lastSigAlgorithmId = sigAlgorithmId;
|
||||
result.add(new SigningCertificateNode(
|
||||
lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
|
||||
SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
|
||||
}
|
||||
} catch(ApkFormatException | BufferUnderflowException e){
|
||||
throw new IOException("Failed to parse SourceStampCertificateLineage object", e);
|
||||
} catch(NoSuchAlgorithmException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException | SignatureException e){
|
||||
throw new SecurityException(
|
||||
"Failed to verify signature over signed data for certificate #" + nodeCount
|
||||
+ " when parsing SourceStampCertificateLineage object", e);
|
||||
} catch(CertificateException e){
|
||||
throw new SecurityException("Failed to decode certificate #" + nodeCount
|
||||
+ " when parsing SourceStampCertificateLineage object", e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents one signing certificate in the SourceStampCertificateLineage, which
|
||||
* generally means it is/was used at some point to sign source stamps.
|
||||
*/
|
||||
public static class SigningCertificateNode {
|
||||
|
||||
public SigningCertificateNode(
|
||||
X509Certificate signingCert,
|
||||
SignatureAlgorithm parentSigAlgorithm,
|
||||
SignatureAlgorithm sigAlgorithm,
|
||||
byte[] signature,
|
||||
int flags) {
|
||||
this.signingCert = signingCert;
|
||||
this.parentSigAlgorithm = parentSigAlgorithm;
|
||||
this.sigAlgorithm = sigAlgorithm;
|
||||
this.signature = signature;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof SigningCertificateNode)) return false;
|
||||
|
||||
SigningCertificateNode that = (SigningCertificateNode) o;
|
||||
if (!signingCert.equals(that.signingCert)) return false;
|
||||
if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
|
||||
if (sigAlgorithm != that.sigAlgorithm) return false;
|
||||
if (!Arrays.equals(signature, that.signature)) return false;
|
||||
if (flags != that.flags) return false;
|
||||
|
||||
// we made it
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode());
|
||||
result = prime * result +
|
||||
((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode());
|
||||
result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode());
|
||||
result = prime * result + Arrays.hashCode(signature);
|
||||
result = prime * result + flags;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* the signing cert for this node. This is part of the data signed by the parent node.
|
||||
*/
|
||||
public final X509Certificate signingCert;
|
||||
|
||||
/**
|
||||
* the algorithm used by this node's parent to bless this data. Its ID value is part of
|
||||
* the data signed by the parent node. {@code null} for first node.
|
||||
*/
|
||||
public final SignatureAlgorithm parentSigAlgorithm;
|
||||
|
||||
/**
|
||||
* the algorithm used by this node to bless the next node's data. Its ID value is part
|
||||
* of the signed data of the next node. {@code null} for the last node.
|
||||
*/
|
||||
public SignatureAlgorithm sigAlgorithm;
|
||||
|
||||
/**
|
||||
* signature over the signed data (above). The signature is from this node's parent
|
||||
* signing certificate, which should correspond to the signing certificate used to sign an
|
||||
* APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
|
||||
*/
|
||||
public final byte[] signature;
|
||||
|
||||
/**
|
||||
* the flags detailing how the platform should treat this signing cert
|
||||
*/
|
||||
public int flags;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SourceStamp signer.
|
||||
*
|
||||
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
|
||||
*
|
||||
* <p>The stamp is part of the APK that is protected by the signing block.
|
||||
*
|
||||
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
|
||||
* block.
|
||||
*
|
||||
* <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
|
||||
*/
|
||||
public abstract class V1SourceStampSigner {
|
||||
public static final int V1_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V1SourceStampSigner() {}
|
||||
|
||||
public static Pair<byte[], Integer> generateSourceStampBlock(
|
||||
SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
|
||||
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||
if (sourceStampSignerConfig.certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates configured for signer");
|
||||
}
|
||||
|
||||
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
|
||||
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
|
||||
}
|
||||
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||
|
||||
SourceStampBlock sourceStampBlock = new SourceStampBlock();
|
||||
|
||||
try {
|
||||
sourceStampBlock.stampCertificate =
|
||||
sourceStampSignerConfig.certificates.get(0).getEncoded();
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new SignatureException(
|
||||
"Retrieving the encoded form of the stamp certificate failed", e);
|
||||
}
|
||||
|
||||
byte[] digestBytes =
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
|
||||
sourceStampBlock.signedDigests =
|
||||
ApkSigningBlockUtils.generateSignaturesOverData(
|
||||
sourceStampSignerConfig, digestBytes);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
|
||||
// * length-prefixed sequence of length-prefixed signatures:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: signature of signed data
|
||||
byte[] sourceStampSignerBlock =
|
||||
encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
sourceStampBlock.stampCertificate,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
sourceStampBlock.signedDigests),
|
||||
});
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed stamp block.
|
||||
return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
|
||||
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID);
|
||||
}
|
||||
|
||||
private static final class SourceStampBlock {
|
||||
public byte[] stampCertificate;
|
||||
public List<Pair<Integer, byte[]>> signedDigests;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
import com.android.apksig.ApkVerifier;
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Source Stamp verifier.
|
||||
*
|
||||
* <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
|
||||
*/
|
||||
public abstract class V1SourceStampVerifier {
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V1SourceStampVerifier() {}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's SourceStamp signatures and returns the result of verification.
|
||||
* The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
|
||||
* {@code true}. If verification fails, the result will contain errors -- see {@link
|
||||
* ApkSigningBlockUtils.Result#getErrors()}.
|
||||
*
|
||||
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||
* required cryptographic algorithm implementation is missing
|
||||
* @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
|
||||
* found
|
||||
* @throws IOException if an I/O error occurs when reading the APK
|
||||
*/
|
||||
public static ApkSigningBlockUtils.Result verify(
|
||||
DataSource apk,
|
||||
ApkUtils.ZipSections zipSections,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws IOException, NoSuchAlgorithmException,
|
||||
ApkSigningBlockUtils.SignatureNotFoundException {
|
||||
ApkSigningBlockUtils.Result result =
|
||||
new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
|
||||
SignatureInfo signatureInfo =
|
||||
ApkSigningBlockUtils.findSignature(
|
||||
apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
|
||||
|
||||
verify(
|
||||
signatureInfo.signatureBlock,
|
||||
sourceStampCertificateDigest,
|
||||
apkContentDigests,
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
|
||||
* {@code result}. APK is considered verified only if there are no errors reported in the {@code
|
||||
* result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
|
||||
* more information about the contract of this method.
|
||||
*/
|
||||
private static void verify(
|
||||
ByteBuffer sourceStampBlock,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion,
|
||||
ApkSigningBlockUtils.Result result)
|
||||
throws NoSuchAlgorithmException {
|
||||
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
|
||||
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||
result.signers.add(signerInfo);
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
ByteBuffer sourceStampBlockData =
|
||||
ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
|
||||
byte[] digestBytes =
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
getApkDigests(apkContentDigests));
|
||||
SourceStampVerifier.verifyV1SourceStamp(
|
||||
sourceStampBlockData,
|
||||
certFactory,
|
||||
signerInfo,
|
||||
digestBytes,
|
||||
sourceStampCertificateDigest,
|
||||
minSdkVersion,
|
||||
maxSdkVersion);
|
||||
result.verified = !result.containsErrors() && !result.containsWarnings();
|
||||
} catch (CertificateException e) {
|
||||
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Pair<Integer, byte[]>> getApkDigests(
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
|
||||
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||
for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
|
||||
apkContentDigests.entrySet()) {
|
||||
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
|
||||
}
|
||||
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||
return digests;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v1;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Digest algorithm used with JAR signing (aka v1 signing scheme).
|
||||
*/
|
||||
public enum DigestAlgorithm {
|
||||
/** SHA-1 */
|
||||
SHA1("SHA-1"),
|
||||
|
||||
/** SHA2-256 */
|
||||
SHA256("SHA-256");
|
||||
|
||||
private final String mJcaMessageDigestAlgorithm;
|
||||
|
||||
private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
|
||||
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link java.security.MessageDigest} algorithm represented by this digest
|
||||
* algorithm.
|
||||
*/
|
||||
String getJcaMessageDigestAlgorithm() {
|
||||
return mJcaMessageDigestAlgorithm;
|
||||
}
|
||||
|
||||
public static Comparator<DigestAlgorithm> BY_STRENGTH_COMPARATOR = new StrengthComparator();
|
||||
|
||||
private static class StrengthComparator implements Comparator<DigestAlgorithm> {
|
||||
@Override
|
||||
public int compare(DigestAlgorithm a1, DigestAlgorithm a2) {
|
||||
switch (a1) {
|
||||
case SHA1:
|
||||
switch (a2) {
|
||||
case SHA1:
|
||||
return 0;
|
||||
case SHA256:
|
||||
return -1;
|
||||
}
|
||||
throw new RuntimeException("Unsupported algorithm: " + a2);
|
||||
|
||||
case SHA256:
|
||||
switch (a2) {
|
||||
case SHA1:
|
||||
return 1;
|
||||
case SHA256:
|
||||
return 0;
|
||||
}
|
||||
throw new RuntimeException("Unsupported algorithm: " + a2);
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unsupported algorithm: " + a1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v1;
|
||||
|
||||
/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */
|
||||
public class V1SchemeConstants {
|
||||
private V1SchemeConstants() {}
|
||||
|
||||
public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
|
||||
public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR =
|
||||
"X-Android-APK-Signed";
|
||||
}
|
||||
@ -0,0 +1,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
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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});
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user