Compare commits
1925 Commits
v4.5.2-252
...
v5.5.4-454
| Author | SHA1 | Date | |
|---|---|---|---|
| 56f817bb7a | |||
| 1b976106e4 | |||
| 0e3ab3c3a7 | |||
| 948588b874 | |||
| 095139079e | |||
| 228ab0ca40 | |||
| fdcb7cbbf7 | |||
| 933f46e250 | |||
| 2282787bd7 | |||
| c4d3698a9b | |||
| 80cc86a791 | |||
| 3646672849 | |||
| 9a4c8f1f06 | |||
| ce303b37b7 | |||
| 0bbffb20cd | |||
| 415626ef58 | |||
| e9ad25dc36 | |||
| 92215163bf | |||
| b3ffe61376 | |||
| 8e6048883a | |||
| 532e40efc3 | |||
| b775083437 | |||
| fc5de50f61 | |||
| b896af2a15 | |||
| 93ba447192 | |||
| 064b041d28 | |||
| a131aca047 | |||
| ad9b39a94a | |||
| c3cf93eaee | |||
| b83f98e90c | |||
| 5e4c451ccd | |||
| ab5e0136a7 | |||
| 89ca11bb81 | |||
| f220b2fd43 | |||
| 28d99cbe38 | |||
| 9c2130562b | |||
| d242aca942 | |||
| a341dcf0b7 | |||
| 8a1c7ed5a0 | |||
| d76d430672 | |||
| 32034473ac | |||
| cf1dc4e4f1 | |||
| 1d9a877f59 | |||
| 772226fb1a | |||
| fe62dee207 | |||
| 37309918dd | |||
| 7f8e853657 | |||
| 006a8a523e | |||
| 8e497122f2 | |||
| 33d5353f75 | |||
| 6da3d46d71 | |||
| a26e61ae4b | |||
| c3d347bffe | |||
| 39cc7e98a8 | |||
| 2a4e500893 | |||
| d6e0cfd644 | |||
| dbbcd29303 | |||
| f3a5f28236 | |||
| bfae7143b1 | |||
| d6f75d6dd0 | |||
| fd9abc7619 | |||
| e3476f9956 | |||
| aedb08ce5d | |||
| 9515676876 | |||
| 4d4d2c91e2 | |||
| 1283ec94de | |||
| 4565819dd8 | |||
| fa5207855e | |||
| 7f586a042b | |||
| 1adad849e0 | |||
| e898b8a7a4 | |||
| 34b46a6143 | |||
| 422cc5d396 | |||
| a048e00489 | |||
| 903276e5ed | |||
| e222f504f6 | |||
| 86707ccc7d | |||
| ed8c1a6b6c | |||
| 545bb6b47d | |||
| 0b40894cda | |||
| e6246314fd | |||
| 2c23f03cc4 | |||
| 91b9502d0b | |||
| 5bbe039e9f | |||
| 8be8fbee19 | |||
| 2e5361f844 | |||
| f4ecf28808 | |||
| 5dd88e30f8 | |||
| 0095c82edb | |||
| b8260cada6 | |||
| 39ec77faae | |||
| 5d4434eebd | |||
| 553ab70d78 | |||
| 23bffe7c5e | |||
| babc41a0cb | |||
| a272033834 | |||
| 82ebdd3908 | |||
| 30cac9f86e | |||
| 6f19a2b668 | |||
| 9235c19475 | |||
| 79d9d05775 | |||
| 2b816a5914 | |||
| 7d163f454e | |||
| b44b676a59 | |||
| c71429750c | |||
| a8c273f68a | |||
| 2588999270 | |||
| 99f7bd9192 | |||
| 70419e412e | |||
| df3df01327 | |||
| 4bdc051d34 | |||
| 575dab06f6 | |||
| 78cc7b116a | |||
| 6ebc4f1f43 | |||
| 82bb0e26ec | |||
| 6c4030c965 | |||
| ba6d3746ab | |||
| 03faa42bab | |||
| 151a4fd2bc | |||
| 3d8c4038a6 | |||
| 1e0265f6ff | |||
| 19871d2bed | |||
| 3da57acf12 | |||
| f6b5e5ecc7 | |||
| 8f471b76eb | |||
| 8218aa004b | |||
| 741a4c1640 | |||
| 63e9f02cda | |||
| 8ed3f759b4 | |||
| 616abbe04c | |||
| 8ea3511451 | |||
| 52475e83e9 | |||
| c573fc3b8f | |||
| bc23240003 | |||
| f92490406e | |||
| 9ab5b67ae7 | |||
| adaffbac77 | |||
| 3c9150adbc | |||
| 884e5b3095 | |||
| 5bafb565a2 | |||
| c750e3a50c | |||
| 917ed615f9 | |||
| 19399824c2 | |||
| 998cfb9233 | |||
| ea86b09444 | |||
| b399e6284c | |||
| adcc6fa5ef | |||
| da3fe16ac0 | |||
| 5f2695f4fb | |||
| 8408091b33 | |||
| 8de2ba1c14 | |||
| 41680a3440 | |||
| 320c44815e | |||
| f4fb26ffa9 | |||
| 39ba62e5bd | |||
| a2a28d801c | |||
| 491c94e536 | |||
| 78a805a6a2 | |||
| 86f6c66f97 | |||
| 674ebbfe65 | |||
| 48d964b957 | |||
| e08ba51951 | |||
| 1c1c271195 | |||
| 9a0d49741d | |||
| 91c07dad2c | |||
| bfb2130517 | |||
| 1942cf8ce9 | |||
| 3f5b5ac2ed | |||
| 7ab424f107 | |||
| 8403db9c0e | |||
| 2139c19b40 | |||
| 34d332129b | |||
| 19617c7e26 | |||
| 1ca9472eee | |||
| 798503cf26 | |||
| fb251f95ce | |||
| 44b69e3d06 | |||
| b44caf693b | |||
| 21c456e1d0 | |||
| aeb41616d9 | |||
| 92b4658123 | |||
| 95c9afa550 | |||
| 0d28fc15e9 | |||
| f5ff23ff3a | |||
| 1b838b473c | |||
| f0c6e437a9 | |||
| 95a955c51e | |||
| f3fe3990c8 | |||
| 8690559646 | |||
| 3e71115443 | |||
| 5534e6f1a7 | |||
| ffe98bfac0 | |||
| 946503d469 | |||
| 381ce29ae9 | |||
| 67bfcc82fa | |||
| 65e07959a5 | |||
| a91195d829 | |||
| 3d75d8507c | |||
| 2ec43d4ff2 | |||
| 7c55f71bb5 | |||
| 99ac44de9e | |||
| 9df38387f2 | |||
| e870dc9fd7 | |||
| d7faf04a2e | |||
| f0e325cd2e | |||
| c64e0da8c7 | |||
| e2140f501f | |||
| 020c8d4456 | |||
| 401c0bfdfe | |||
| 33c411befa | |||
| 041619ed0d | |||
| ad6114a44b | |||
| d61008349a | |||
| 39cfa01f5a | |||
| 924f83dacf | |||
| 68db40c4c8 | |||
| b53a793e1c | |||
| 1097300211 | |||
| 1052663db0 | |||
| e3f0311cc7 | |||
| f0af5787c3 | |||
| 4ed1da1777 | |||
| 6cca81c4bb | |||
| 047c45db9a | |||
| 689fec168f | |||
| cff7f4a9f5 | |||
| fcb8a60722 | |||
| f2ad9b2235 | |||
| 49067706b7 | |||
| 098f02f0aa | |||
| 772aa7ca03 | |||
| 4e78162d7f | |||
| 017600f90e | |||
| 86d1749989 | |||
| 32bc1a4a6f | |||
| 58b37f8922 | |||
| 135b55b204 | |||
| 1f025f3556 | |||
| 54bf1af524 | |||
| 11cae2c56f | |||
| 7075f3bcc1 | |||
| 5df2a1c2f5 | |||
| 831ca71a58 | |||
| bb854161f5 | |||
| a98d6125da | |||
| b178961b9b | |||
| 9c6664aed2 | |||
| 1d254248d9 | |||
| 844c028b7b | |||
| cf320fa01b | |||
| 739b729654 | |||
| 48729b298f | |||
| df2d5a93a0 | |||
| 328d200a78 | |||
| a5f5bd8828 | |||
| 760aa6f8f6 | |||
| d6a341337a | |||
| 82d79028de | |||
| 92eece6289 | |||
| edd8b86144 | |||
| 5d5c8dfddb | |||
| 1dc2e661d1 | |||
| feb46a5887 | |||
| 4086fcdb30 | |||
| 8dd8d21e85 | |||
| 42a4210290 | |||
| 9a1266d9d9 | |||
| 2915042719 | |||
| b4730589bf | |||
| 001cbfcdb6 | |||
| 44dab8246f | |||
| be5cbf0ca0 | |||
| fa2fb66f44 | |||
| 9982ebea9e | |||
| 2b30cc1f41 | |||
| 96a19eb5ee | |||
| 85276d462f | |||
| 44aebc39ea | |||
| 52294ebb3a | |||
| 623156bdb5 | |||
| 6bac5d79a9 | |||
| 293fc635e1 | |||
| 0f0dc6dd9b | |||
| 46de8f5f9e | |||
| ed6b2960cc | |||
| f3ef7930f3 | |||
| 5ecb3d5378 | |||
| 1d4fe89900 | |||
| 3f3b19f2cf | |||
| af1ac4e888 | |||
| 1478b59479 | |||
| 335cde94fe | |||
| dd1f399425 | |||
| 3e8923f34a | |||
| 612fc8f444 | |||
| 0cbc104092 | |||
| 41f7e3a05a | |||
| 25ce3ccdc9 | |||
| 82a60413e1 | |||
| 2b4126fa71 | |||
| 52a7ab7b3c | |||
| 9b8ce918d8 | |||
| 57081f4003 | |||
| 5df0d85004 | |||
| 63b88383a4 | |||
| 7564a0a771 | |||
| 36bc35a7e3 | |||
| 1c55ab8f86 | |||
| 9a1f3b0fe3 | |||
| 438b55c08b | |||
| 12fcb98412 | |||
| 33030cf550 | |||
| 9a7acef9a3 | |||
| 61f92e544b | |||
| 57d6cc7d0e | |||
| 919ace95b9 | |||
| ec1af285af | |||
| 6b4c02e965 | |||
| 913bcdeeb2 | |||
| e40857d24b | |||
| d3fab1aa73 | |||
| d32698ca91 | |||
| 7832276560 | |||
| 2fca73dfe8 | |||
| 0f194a80bb | |||
| e0cdf42d26 | |||
| c501248408 | |||
| a559df99ec | |||
| 1cdd7ed6ef | |||
| 7e3bd14db4 | |||
| ab08a725ca | |||
| 1977c80038 | |||
| d9d8eaa611 | |||
| e0dcb70fcd | |||
| bb60f7f22a | |||
| 70821c73c1 | |||
| 1560234105 | |||
| 59f74ebb9e | |||
| d3e21251d9 | |||
| aedd996c3f | |||
| a589b77de5 | |||
| 86330c74b5 | |||
| 54a77555cc | |||
| 30ae815717 | |||
| 1eb1b6a956 | |||
| f46b303a67 | |||
| a1ecb784e2 | |||
| 142e04fa60 | |||
| 540d6cd1e4 | |||
| b078f38e62 | |||
| a2e5415b5c | |||
| 9aae1c5476 | |||
| e73b431378 | |||
| 0fe60f52e2 | |||
| 6957ca54f0 | |||
| 31de4089ad | |||
| d73a4704c8 | |||
| 2f5f52471e | |||
| 97f82d70f5 | |||
| c355bc2b79 | |||
| a011b5a947 | |||
| efdf02158c | |||
| 4ca302f5fc | |||
| e669df00b4 | |||
| 19e6f1e7b7 | |||
| 025d237be0 | |||
| 2eb5fb7526 | |||
| 3818e17976 | |||
| 1f43b8b220 | |||
| 10003fe49d | |||
| 80a8cadaae | |||
| 88098a8255 | |||
| 66a543146a | |||
| 3aafc3fcbe | |||
| 050da95a8f | |||
| 40b7b09de4 | |||
| 16091f759c | |||
| f65664b885 | |||
| 3eef9c465a | |||
| a1b4c726dd | |||
| 5e2752569b | |||
| 65a823698a | |||
| fdfad0edf1 | |||
| dcadb529c7 | |||
| 37460ceac2 | |||
| 994397ec35 | |||
| 9354ff9af8 | |||
| 2811e21911 | |||
| 746f81e45b | |||
| 69f1887926 | |||
| 6dfded50d6 | |||
| 5e01ecf0a2 | |||
| a1be42a135 | |||
| 5bdd655715 | |||
| c32e9bd0fc | |||
| 2e2985cb90 | |||
| f4a5dd47fc | |||
| 81873bd898 | |||
| d6604ff2c9 | |||
| 298b7af692 | |||
| 433df3bebd | |||
| 84dba2036c | |||
| 924ef2a727 | |||
| 5a2b997693 | |||
| ebb87962ac | |||
| 075eb1a21b | |||
| b9554d11a8 | |||
| b910075c4c | |||
| 1966e844bd | |||
| e760a63181 | |||
| bfa236f802 | |||
| 7f64b51582 | |||
| 4324126660 | |||
| a8c54de47d | |||
| 562b3e9d3b | |||
| 679c6da972 | |||
| 793adb9a03 | |||
| 23c62f1092 | |||
| 70acea8e92 | |||
| 516d8d8044 | |||
| ed2b41bc55 | |||
| 5351ffd6b6 | |||
| aaffbb376c | |||
| ac852c0484 | |||
| 4bd93ad958 | |||
| 5f131848e7 | |||
| 4915f99412 | |||
| 92841346cd | |||
| b0e8db6e0e | |||
| d533e0ceab | |||
| 672b697209 | |||
| 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 | |||
| ca190c6d57 | |||
| 1c739a6269 | |||
| f5386cd3c1 | |||
| 65aab99639 | |||
| 7540659396 | |||
| bbb7904477 | |||
| 056985531b | |||
| 730739077b | |||
| 6c82184d1d | |||
| d3b6f6a1e4 | |||
| d35550900b | |||
| dfc16e38eb | |||
| 08bd5f3081 | |||
| 7d98fde53a | |||
| 486e9ae0c5 | |||
| d51587aa66 | |||
| 1fabcc756d | |||
| ee3fc058e1 | |||
| e66e9e1ba8 | |||
| 1cb4bea73c | |||
| b3a491e723 | |||
| b583aafa25 | |||
| 9709c6cbf0 | |||
| ca81eb2ecb | |||
| 910d886f65 | |||
| 376411c0cf | |||
| ad019eff69 | |||
| 3712e67594 | |||
| c98084ec39 | |||
| 5a851ea99d | |||
| 849cdb234c | |||
| 9f9313b5e7 | |||
| 1888d8cfc7 | |||
| 393b747f0e | |||
| d8f9a7ed7f | |||
| 7be4541f89 | |||
| 97e87a9f32 | |||
| 2d84309f04 | |||
| f2ed7f8b72 | |||
| c839d535d8 | |||
| 623e155c75 | |||
| b20a74de00 | |||
| 090b5fe04f | |||
| b7dca9b652 | |||
| c9402dec4e | |||
| 6dc097f60f | |||
| 636a826583 | |||
| 5c313ab7ac | |||
| 4aacc425a9 | |||
| c7db544684 | |||
| c7833bae11 | |||
| 3d777781b8 | |||
| f2501eeb3b | |||
| b72f9819d7 | |||
| 5f134fad33 | |||
| 21cec51fad | |||
| 048d3aa361 | |||
| 8871d12fa8 | |||
| 67f931f9c8 | |||
| 2fe9d6c3e1 | |||
| a662d5b90c | |||
| c30ec4bd87 | |||
| ee0a3a9d05 | |||
| 094a8abfe2 | |||
| e1c39e90a9 | |||
| aca3f2d2fc | |||
| 05d95259e2 | |||
| c278359909 | |||
| b5d8d40462 | |||
| c1b7c380da | |||
| e4f0ba0495 | |||
| ccd4a479a9 | |||
| 92941bcc38 | |||
| 6b55875547 | |||
| a0947a84d9 | |||
| fca0e03180 | |||
| 3dad7d8850 | |||
| bc18301dc1 | |||
| 1ebe8eba4e | |||
| 3ec3388b08 | |||
| 71c2e8eb6b | |||
| 009aa9f73e | |||
| 14e38bc738 | |||
| 74ffd80b10 | |||
| cb7a04ff09 | |||
| 96640ad91e | |||
| 3791dfd558 | |||
| 91017caac4 | |||
| 5d8720687a | |||
| 75b1234376 | |||
| 1e866a9eab | |||
| 80e58080c4 | |||
| 394036a0d0 | |||
| 2f2355f36e | |||
| e711e30c21 | |||
| e3d32057ee | |||
| e42c4e595e | |||
| 73d71e5dac | |||
| d6cad14e7b | |||
| 9beaf93595 | |||
| 074610a9f0 | |||
| 7881edaf21 | |||
| f2643308f9 | |||
| 1168be9a92 | |||
| c34d6141bc | |||
| e1f62e3a3a | |||
| 1d067dbaa2 | |||
| f1b6e742d9 | |||
| 149448c04c | |||
| e388a60d43 | |||
| ea49ef2691 | |||
| 741b2c23d2 | |||
| dca3542cec | |||
| 4a37af4ce4 | |||
| a2e907faeb | |||
| 7e33243fbb | |||
| 2b0abb1740 | |||
| 60e4f2a378 | |||
| a4603213b9 | |||
| da9ea61fbf | |||
| 4ceef741ea | |||
| d323fc07f9 | |||
| 2356e175f9 | |||
| 7a9c0ed8f2 | |||
| d406cf3fcb | |||
| 9741cac1cf | |||
| 456519e06c | |||
| bad6b495b1 | |||
| 638e0c988f | |||
| 40aaabad7d | |||
| 46cf8cfbdc | |||
| 2226bcf515 | |||
| 27ceb7b042 | |||
| c2226f8452 | |||
| 38dcdaf8e3 | |||
| 2a440e91fa | |||
| 2cecb9a14c | |||
| 241f976866 | |||
| 560c270723 | |||
| 5159957722 | |||
| cfb851c23e | |||
| e45218de17 | |||
| 0ff34bd3f7 | |||
| 6bc7a9f504 | |||
| 7ea53f305d | |||
| 4a04f6bb0f | |||
| 633b7df021 | |||
| 4ddaccd3f4 | |||
| 389c20b6bf | |||
| 20149b6041 | |||
| 2f6af6c18e | |||
| 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 | |||
| eb2228fb17 | |||
| 0e27e8ee3b | |||
| 17455efe82 | |||
| e2c80cb416 | |||
| a48b4db550 | |||
| 1d66637b11 | |||
| 3e7e98d555 | |||
| 60e542923c | |||
| e737d16f0f | |||
| a04219518b | |||
| ce2c6bed0a | |||
| 84a08493e3 | |||
| 3d411fa49c | |||
| c0a0c90a49 | |||
| a67d41296e | |||
| 25a95ddba0 | |||
| fac255a27c | |||
| 0811c8dc15 | |||
| 7537963a8e | |||
| 36f8b1de0b | |||
| 1d9e1bc9d8 | |||
| bb9fd24068 | |||
| 9c044ae98b | |||
| 9807883d65 | |||
| 04e410615d | |||
| eda1eea2de | |||
| c1f6ef35f2 | |||
| b61ce1e42a | |||
| 62e60f4309 | |||
| c6f544136f | |||
| d588315759 | |||
| b1fef73c54 | |||
| c4fc31c963 | |||
| 85b0d0eef4 | |||
| 812eb842e2 | |||
| 783cb95f24 | |||
| 63b7f294c4 | |||
| 80c95b0f11 | |||
| 5969fa2ca5 | |||
| 0187608918 | |||
| f7b155d3b4 | |||
| 82656a7187 | |||
| 9993824d93 | |||
| e2fa4990bd | |||
| 6753d04817 | |||
| 9679c75f0a | |||
| c203fa5b9d | |||
| 98531376d9 | |||
| 86edc8b919 | |||
| 7903751d85 | |||
| 2620a29a2b | |||
| 5cc40c09dc | |||
| 5c02d37852 | |||
| 2e5d445d65 | |||
| 456822f56f | |||
| c8bae7d89b | |||
| a5aceb3d1f | |||
| 578ed5d1f1 | |||
| 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 | |||
| d01fda44b3 | |||
| 2020033bc0 | |||
| dc2c8e590c | |||
| b115db51e9 | |||
| 7d194c7078 |
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
|
||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@ -1,4 +1,7 @@
|
||||
[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
|
||||
[submodule "assistant_flutter"]
|
||||
path = assistant_flutter
|
||||
url = git@git.ghzs.com:halo/android/flutter-module.git
|
||||
|
||||
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
|
||||
|
||||
345
app/build.gradle
345
app/build.gradle
@ -1,27 +1,21 @@
|
||||
// This comment exists for a reason, do not delete
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android' // kotlin
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
// apkChannelPackage
|
||||
apply plugin: 'channel'
|
||||
apply plugin: 'AndResGuard'
|
||||
|
||||
import groovy.xml.XmlUtil
|
||||
|
||||
//apply from: 'tinker-support.gradle'
|
||||
|
||||
android {
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
viewBinding {
|
||||
enabled = true
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@ -36,6 +30,20 @@ android {
|
||||
dexOptions {
|
||||
// jumboMode = true
|
||||
javaMaxHeapSize "4g"
|
||||
preDexLibraries true
|
||||
maxProcessCount 8
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
additionalParameters "--no-version-vectors"
|
||||
}
|
||||
|
||||
kapt {
|
||||
useBuildCache = true
|
||||
javacOptions {
|
||||
// 增加注解处理器的最大错误次数,默认为 100
|
||||
option("-Xmaxerrs", 500)
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@ -48,7 +56,12 @@ android {
|
||||
}
|
||||
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a"
|
||||
// 如果不添加 `arm64` 调用系统的 PackageManager 的方法读取安装包信息的时候会出现 native 层闪退,草
|
||||
// 添加了 `arm64` 以后部分 5.0 的设备会报用错 so 的问题,
|
||||
// couldn't find DSO to load: libimagepipeline.so caused by: dlopen failed: "/data/data/com.gh.gamecenter/lib-main/libimagepipeline.so" is 64-bit instead of 32-bit result: 0
|
||||
// 以 OPPO R7PLUS 为例,明明设备是骁龙 615,ARMv8-64 bit 的设备却不支持 arm64 的 abi,限制了只使用 java 后还是报错,只有 5.0,5.1 设备无法复现 : (
|
||||
// 惊了
|
||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
||||
}
|
||||
|
||||
renderscriptTargetApi 18
|
||||
@ -68,29 +81,24 @@ android {
|
||||
/**
|
||||
* All third-party appid/appkey
|
||||
*/
|
||||
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
|
||||
buildConfigField "String", "NEW_API_HOST", "\"${NEW_API_HOST}\""
|
||||
buildConfigField "String", "WECHAT_APPID", "\"${WECHAT_APPID}\""
|
||||
buildConfigField "String", "WECHAT_SECRET", "\"${WECHAT_SECRET}\""
|
||||
buildConfigField "String", "TENCENT_APPID", "\"${TENCENT_APPID}\""
|
||||
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", "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}"
|
||||
buildConfigField "String", "QUICK_LOGIN_APPID", "\"${QUICK_LOGIN_APPID}\""
|
||||
buildConfigField "String", "QUICK_LOGIN_APPKEY", "\"${QUICK_LOGIN_APPKEY}\""
|
||||
|
||||
/**
|
||||
* Build Time 供区分 jenkins 打包时间用
|
||||
*/
|
||||
buildConfigField "long", "BUILD_TIME", "0"
|
||||
|
||||
}
|
||||
|
||||
// gradle 2.2以上默认同时启用v1和v2(优先用于Android N)
|
||||
@ -131,6 +139,15 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore useless variant
|
||||
variantFilter { variant ->
|
||||
def names = variant.flavors*.name
|
||||
def isDebugType = variant.buildType.name == "debug"
|
||||
if ((names.contains("tea") || name.contains("gdt")) && isDebugType) {
|
||||
setIgnore(true)
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions("env")
|
||||
|
||||
sourceSets {
|
||||
@ -149,72 +166,44 @@ android {
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
// publish release host
|
||||
publish {
|
||||
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}\""
|
||||
}
|
||||
// internal test dev host
|
||||
internal {
|
||||
dimension "env"
|
||||
versionNameSuffix "-debug"
|
||||
|
||||
buildConfigField "String", "API_HOST", "\"${DEV_API_HOST}\""
|
||||
buildConfigField "String", "DEV_API_HOST", "\"${DEV_API_HOST}\""
|
||||
buildConfigField "String", "NEW_DEV_API_HOST", "\"${NEW_DEV_API_HOST}\""
|
||||
}
|
||||
|
||||
buildConfigField "String", "SENSITIVE_API_HOST", "\"${DEV_API_HOST}\""
|
||||
// publish release host˛
|
||||
publish {
|
||||
dimension "env"
|
||||
|
||||
buildConfigField "String", "UMENG_APPKEY", "\"${DEV_UMENG_APPKEY}\""
|
||||
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${DEV_UMENG_MESSAGE_SECRET}\""
|
||||
buildConfigField "String", "BUGLY_APPID", "\"${DEV_BUGLY_APPID}\""
|
||||
buildConfigField "String", "DEV_API_HOST", "\"${API_HOST}\""
|
||||
buildConfigField "String", "NEW_DEV_API_HOST", "\"${NEW_API_HOST}\""
|
||||
}
|
||||
|
||||
tea {
|
||||
dimension "env"
|
||||
|
||||
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
|
||||
buildConfigField "String", "DEV_API_HOST", "\"${API_HOST}\""
|
||||
buildConfigField "String", "NEW_DEV_API_HOST", "\"${NEW_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}\""
|
||||
buildConfigField "String", "DEV_API_HOST", "\"${API_HOST}\""
|
||||
buildConfigField "String", "NEW_DEV_API_HOST", "\"${NEW_API_HOST}\""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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渠道包输出目录
|
||||
lintOptions {
|
||||
// For flutter release build, see https://github.com/flutter/flutter/issues/58247
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
@ -231,14 +220,12 @@ dependencies {
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakcanary}"
|
||||
debugImplementation "com.facebook.stetho:stetho:${stetho}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stetho}"
|
||||
debugImplementation "com.squareup.okhttp3:logging-interceptor:${okHttp}"
|
||||
debugImplementation "com.gu.android:toolargetool:${toolargetool}"
|
||||
debugImplementation "com.github.nichbar:WhatTheStack:$whatTheStack"
|
||||
// debugImplementation "com.gu.android:toolargetool:${toolargetool}" // 需要使用调试时才启用
|
||||
debugImplementation "com.github.nichbar:WhatTheStack:${whatTheStack}"
|
||||
|
||||
implementation "androidx.core:core:${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}"
|
||||
@ -252,6 +239,7 @@ dependencies {
|
||||
implementation "androidx.room:room-rxjava2:${room}"
|
||||
implementation "androidx.core:core-ktx:${ktx}"
|
||||
implementation "androidx.viewpager2:viewpager2:${viewpager2}"
|
||||
implementation "androidx.webkit:webkit:${webkit}"
|
||||
kapt "androidx.room:room-compiler:${room}"
|
||||
|
||||
implementation "com.google.android.material:material:${material}"
|
||||
@ -259,8 +247,10 @@ dependencies {
|
||||
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}"
|
||||
|
||||
@ -289,11 +279,6 @@ dependencies {
|
||||
|
||||
implementation "com.daimajia.swipelayout:library:${swipeLayout}"
|
||||
|
||||
implementation "com.sina.weibo.sdk:core:${weiboSDK}"
|
||||
|
||||
// bugly with tinker support
|
||||
// implementation "com.tencent.bugly:crashreport_upgrade:${buglyTinkerSupport}"
|
||||
|
||||
implementation "com.google.android:flexbox:${flexbox}"
|
||||
|
||||
implementation "pub.devrel:easypermissions:${easypermissions}"
|
||||
@ -313,11 +298,11 @@ dependencies {
|
||||
implementation "com.squareup.picasso:picasso:${picasso}"
|
||||
|
||||
// for video streaming
|
||||
implementation("com.shuyu:gsyVideoPlayer-java:$gsyVideo", {
|
||||
implementation("com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:$gsyVideo", {
|
||||
exclude module: "gsyvideoplayer-androidvideocache"
|
||||
exclude group: "tv.danmaku.ijk.media"
|
||||
})
|
||||
implementation "com.shuyu:GSYVideoPlayer-exo2:$gsyVideo"
|
||||
implementation "com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-exo_player2:$gsyVideo"
|
||||
|
||||
implementation "android.arch.work:work-runtime:${workManager}"
|
||||
|
||||
@ -325,39 +310,50 @@ dependencies {
|
||||
|
||||
implementation "com.github.tbruyelle:rxpermissions:${rxPermissions}"
|
||||
|
||||
implementation "com.ethanhua:skeleton:${skeleton}"
|
||||
implementation "io.supercharge:shimmerlayout:${shimmerlayout}"
|
||||
implementation "com.lg:skeleton:${skeleton}"
|
||||
implementation "com.tencent.mm.opensdk:wechat-sdk-android-without-mta:${mta}"
|
||||
implementation "com.walkud.rom.checker:RomChecker:${romChecker}"
|
||||
implementation "com.github.nichbar:AndroidRomChecker:${romChecker}"
|
||||
|
||||
debugImplementation "com.github.nichbar.chucker:library:$chucker"
|
||||
releaseImplementation "com.github.nichbar.chucker:library-no-op:$chucker"
|
||||
teaImplementation "com.bytedance.applog:RangersAppLog-Lite-cn:$bytedanceApplog"
|
||||
// implementation "com.bytedance.ies.ugc.aweme:opensdk-china-external:$bytedanceAweme"
|
||||
// implementation "com.bytedance.ies.ugc.aweme:opensdk-common:$bytedanceAweme"
|
||||
debugImplementation "com.github.nichbar.chucker:library:${chucker}"
|
||||
releaseImplementation "com.github.nichbar.chucker:library-no-op:${chucker}"
|
||||
teaImplementation "com.bytedance.applog:RangersAppLog-Lite-cn:${bytedanceApplog}"
|
||||
|
||||
implementation "com.aliyun.dpa:oss-android-sdk:${oss}"
|
||||
|
||||
implementation "com.airbnb.android:lottie:$lottie"
|
||||
implementation "com.airbnb.android:lottie:${lottie}"
|
||||
|
||||
implementation "net.lingala.zip4j:zip4j:${zip4j}"
|
||||
|
||||
implementation "io.sentry:sentry-android:$sentry"
|
||||
implementation "io.sentry:sentry-android:4.3.0"
|
||||
|
||||
implementation("com.github.piasy:BigImageViewer:$bigImageViewer", {
|
||||
implementation("com.github.piasy:BigImageViewer:${bigImageViewer}", {
|
||||
exclude group: 'com.squareup.okhttp3'
|
||||
exclude group: 'androidx.swiperefreshlayout'
|
||||
exclude group: 'com.github.bumptech.glide'
|
||||
exclude group: 'com.facebook.fresco'
|
||||
})
|
||||
implementation "com.github.PhilJay:MPAndroidChart:${chart}"
|
||||
|
||||
implementation "com.lahm.library:easy-protector-release:${easyProtector}"
|
||||
|
||||
implementation "com.github.hsiafan:apk-parser:${apkParser}"
|
||||
implementation "org.nanohttpd:nanohttpd:${nanohttpd}"
|
||||
|
||||
implementation "com.aliyun.openservices:aliyun-log-android-sdk:${aliyunLog}"
|
||||
implementation "com.lg:easyfloat:${easyFloat}"
|
||||
|
||||
implementation "io.github.florent37:shapeofview:${shapeOfView}"
|
||||
|
||||
implementation "io.github.sinaweibosdk:core:${weiboSDK}"
|
||||
|
||||
implementation "com.lg:gid:1.3.0"
|
||||
|
||||
implementation "com.louiscad.splitties:splitties-fun-pack-android-base-with-views-dsl:${splitties}"
|
||||
|
||||
compileOnly "com.github.axen1314.lancet:lancet-base:$lancet_version"
|
||||
|
||||
implementation project(':libraries:LGLibrary')
|
||||
// implementation project(':libraries:MTA')
|
||||
implementation project(':libraries:QQShare')
|
||||
// implementation project(':libraries:TalkingData')
|
||||
// implementation project(':libraries:UmengPush')
|
||||
// implementation project(':libraries:WechatShare')
|
||||
// implementation project(':libraries:im')
|
||||
implementation project(':libraries:Matisse')
|
||||
}
|
||||
File propFile = file('sign.properties')
|
||||
@ -417,6 +413,111 @@ if (propFile.exists()) {
|
||||
// }.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.ic_bar_back",
|
||||
"R.drawable.toolbar_search_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",
|
||||
"R.id.download_speed",
|
||||
"R.id.download_percentage",
|
||||
"R.id.comment",
|
||||
"R.id.vote",
|
||||
"R.id.watermark_hint",
|
||||
"R.id.watermark_sb",
|
||||
"R.id.bottomShareIv",
|
||||
"R.id.bottomShareTv",
|
||||
"R.id.recommendStarPref",
|
||||
"R.id.recommendStar",
|
||||
"R.drawable.help_search_delete",
|
||||
"R.drawable.suggest_type_normal",
|
||||
"R.drawable.suggest_type_crash",
|
||||
"R.drawable.suggest_type_game_question",
|
||||
"R.drawable.suggest_type_game_collect",
|
||||
"R.drawable.suggest_type_function_suggest",
|
||||
"R.drawable.suggest_type_article_collect",
|
||||
"R.drawable.suggest_type_copyright",
|
||||
"R.drawable.help_result_empty",
|
||||
"R.drawable.news_comment_detail_read",
|
||||
"R.drawable.news_comment_detail_comment",
|
||||
"R.drawable.news_comment_detail_share",
|
||||
"R.drawable.ic_libao",
|
||||
"R.drawable.ic_link",
|
||||
"R.drawable.concern_message_icon",
|
||||
"R.drawable.reuse_blank_hint",
|
||||
"R.drawable.ic_concern",
|
||||
"R.drawable.concern_down",
|
||||
"R.drawable.concern_up",
|
||||
"R.drawable.ic_libao_more",
|
||||
"R.drawable.ic_libao_delete",
|
||||
"R.drawable.ic_dialog_close"
|
||||
]
|
||||
compressFilePattern = [
|
||||
"*.png",
|
||||
"*.jpg",
|
||||
"*.jpeg",
|
||||
"*.gif",
|
||||
]
|
||||
sevenzip {
|
||||
artifact = 'com.tencent.mm:SevenZip:1.2.20'
|
||||
}
|
||||
}
|
||||
|
||||
project.afterEvaluate {
|
||||
def variants = null
|
||||
@ -448,36 +549,46 @@ project.afterEvaluate {
|
||||
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()) {
|
||||
value = "keyboardHidden|orientation|screenSize|screenLayout|density|fontScale|locale"
|
||||
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("\\|")
|
||||
if (!valueSplit.contains("keyboardHidden")) {
|
||||
value += "|keyboardHidden"
|
||||
}
|
||||
if (!valueSplit.contains("orientation")) {
|
||||
value += "|orientation"
|
||||
}
|
||||
if (!valueSplit.contains("screenSize")) {
|
||||
value += "|screenSize"
|
||||
}
|
||||
if (!valueSplit.contains("screenLayout")) {
|
||||
value += "|screenLayout"
|
||||
}
|
||||
if (!valueSplit.contains("density")) {
|
||||
value += "|density"
|
||||
}
|
||||
if (!valueSplit.contains("fontScale")) {
|
||||
value += "|fontScale"
|
||||
}
|
||||
if (!valueSplit.contains("locale")) {
|
||||
value += "|locale"
|
||||
println configChanges
|
||||
configChanges.eachWithIndex { config, index ->
|
||||
if (!valueSplit.contains(config)) {
|
||||
value += ("|" + config)
|
||||
}
|
||||
}
|
||||
act.attributes()['android:configChanges'] = value
|
||||
}
|
||||
|
||||
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,67 @@
|
||||
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.* {*;}
|
||||
-keep class com.gh.gamecenter.home.gamecollection.* {*;}
|
||||
|
||||
###
|
||||
-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,93 +77,38 @@
|
||||
-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.** { *; }
|
||||
-keep class tv.danmaku.ijk.* { *; }
|
||||
-dontwarn tv.danmaku.ijk.**
|
||||
-keep public class * extends android.view.View{
|
||||
*** get*();
|
||||
@ -249,23 +118,34 @@
|
||||
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.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.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(...);
|
||||
}
|
||||
|
||||
# Flutter模块
|
||||
-keep class com.gh.common.util.DirectUtils {
|
||||
public static void directToQa(...);
|
||||
public static void directToQaCollection(...);
|
||||
public static void directToFeedback(...);
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,6 @@ package com.gh.gamecenter;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.facebook.stetho.Stetho;
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
@ -17,9 +15,6 @@ import okhttp3.logging.HttpLoggingInterceptor;
|
||||
public class Injection {
|
||||
|
||||
public static boolean appInit(Application application) {
|
||||
// init stetho
|
||||
Stetho.initializeWithDefaults(application);
|
||||
|
||||
// 监控Bundle大小,预防溢出(需要调试的时候再开启吧!)
|
||||
// TooLargeTool.startLogging(application);
|
||||
return true;
|
||||
@ -30,7 +25,6 @@ public class Injection {
|
||||
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
|
||||
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
builder.addNetworkInterceptor(interceptor);
|
||||
builder.addNetworkInterceptor(new StethoInterceptor());
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@ -31,6 +31,8 @@ object GdtHelper {
|
||||
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")
|
||||
}
|
||||
|
||||
@ -21,23 +21,17 @@
|
||||
<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.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" />
|
||||
|
||||
<!--可选,穿山甲提供“获取地理位置权限”和“不给予地理位置权限,开发者传入地理位置参数”两种方式上报用户位置,两种方式均可不选,添加位置权限或参数将帮助投放定位广告-->
|
||||
<!--请注意:无论通过何种方式提供给穿山甲用户地理位置,均需向用户声明地理位置权限将应用于穿山甲广告投放,穿山甲不强制获取地理位置信息-->
|
||||
<!--<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />-->
|
||||
|
||||
<!-- 如果有视频相关的广告且使用textureView播放,请务必添加,否则黑屏 -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
@ -47,9 +41,11 @@
|
||||
com.shuyu.gsyvideoplayer.armv7a,
|
||||
com.shuyu.gsyvideoplayer.x86,
|
||||
com.shuyu.gsy.base,
|
||||
shuyu.com.androidvideocache,
|
||||
com.google.android.exoplayer2,
|
||||
tv.danmaku.ijk.media.exo2,
|
||||
pl.droidsonroids.gif" />
|
||||
pl.droidsonroids.gif,
|
||||
com.lzf.easyfloat" />
|
||||
|
||||
<!-- 去掉 SDK 一些流氓权限 -->
|
||||
<uses-permission
|
||||
@ -70,16 +66,21 @@
|
||||
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:replace="android:name,android:allowBackup"
|
||||
tools:targetApi="n">
|
||||
|
||||
<meta-data
|
||||
android:name="io.sentry.auto-init"
|
||||
android:value="false" />
|
||||
|
||||
<!--android:launchMode = "singleTask"-->
|
||||
<!-- 不让 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"
|
||||
@ -106,7 +107,6 @@
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<!--android:theme = "@android:style/Theme.Black.NoTitleBar.Fullscreen" 退出时屏幕抖动 -->
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.ImageViewerActivity"
|
||||
android:theme="@style/Theme.Transparent" />
|
||||
@ -164,6 +164,10 @@
|
||||
android:name="com.gh.gamecenter.WebActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SingletonWebActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.FullScreenWebActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
@ -469,7 +473,7 @@
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.qa.editor.VideoActivity"
|
||||
android:name="com.gh.gamecenter.qa.editor.LocalMediaActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
@ -507,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"
|
||||
@ -546,10 +556,6 @@
|
||||
android:name=".forum.select.ForumSelectActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".forum.follow.ForumMyFollowActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".forum.detail.ForumDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
@ -558,6 +564,10 @@
|
||||
android:name=".forum.moderator.ModeratorListActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".forum.moderator.ApplyModeratorActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".video.label.VideoLabelActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
@ -579,10 +589,6 @@
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/TransparentStatusBarAndNavigationBar" />
|
||||
|
||||
<activity
|
||||
android:name=".personalhome.excellentcomments.ExcellentCommentsActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".simulatorgame.SimulatorGameActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
@ -603,6 +609,100 @@
|
||||
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=".gamecollection.detail.GameCollectionDetailActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".gamecollection.detail.GameCollectionPosterActivity"
|
||||
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"-->
|
||||
@ -630,6 +730,39 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.teenagermode.TeenagerModeActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".gamecollection.publish.GameCollectionEditActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".gamecollection.choose.ChooseGamesActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".gamecollection.choose.AddGamesActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".gamecollection.mine.MyGameCollectionActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.gamecollection.square.GameCollectionSquareActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.gamecollection.tag.GameCollectionTagSelectActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".qa.editor.InsertGameCollectionWrapperActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="${applicationId}.wxapi.WXEntryActivity"
|
||||
android:exported="true"
|
||||
@ -654,6 +787,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">
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
3329
app/src/main/java/com/android/apksig/ApkVerifier.java
Normal file
3329
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,463 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.oid;
|
||||
|
||||
import com.android.apksig.internal.util.InclusiveIntRange;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class OidConstants {
|
||||
public static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
|
||||
public static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
|
||||
public static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
|
||||
public static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
|
||||
public static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
|
||||
public static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
|
||||
|
||||
public static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
|
||||
public static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
|
||||
public static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
|
||||
public static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
|
||||
public static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
|
||||
public static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
|
||||
public static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
|
||||
|
||||
public static final String OID_SIG_DSA = "1.2.840.10040.4.1";
|
||||
public static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
|
||||
public static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
|
||||
public static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
|
||||
public static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
|
||||
public static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
|
||||
|
||||
public static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
|
||||
public static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
|
||||
public static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
|
||||
public static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
|
||||
public static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
|
||||
public static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
|
||||
|
||||
public static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
|
||||
new HashMap<>();
|
||||
static {
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_RSA,
|
||||
InclusiveIntRange.from(0));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_RSA,
|
||||
InclusiveIntRange.from(0));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
|
||||
InclusiveIntRange.from(0));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_RSA,
|
||||
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_RSA,
|
||||
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_RSA,
|
||||
InclusiveIntRange.from(18));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
|
||||
InclusiveIntRange.from(21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_RSA,
|
||||
InclusiveIntRange.from(18));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
|
||||
InclusiveIntRange.fromTo(21, 21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
|
||||
InclusiveIntRange.from(21));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_DSA,
|
||||
InclusiveIntRange.from(0));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
|
||||
InclusiveIntRange.from(9));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_DSA,
|
||||
InclusiveIntRange.from(22));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
|
||||
InclusiveIntRange.from(21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_DSA,
|
||||
InclusiveIntRange.from(22));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
|
||||
InclusiveIntRange.from(21));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
|
||||
InclusiveIntRange.from(18));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
|
||||
InclusiveIntRange.from(21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
|
||||
InclusiveIntRange.from(18));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
|
||||
InclusiveIntRange.from(18));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
|
||||
InclusiveIntRange.from(18));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
|
||||
InclusiveIntRange.from(18));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
|
||||
InclusiveIntRange.from(21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
|
||||
InclusiveIntRange.from(21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
|
||||
InclusiveIntRange.from(21));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
|
||||
InclusiveIntRange.fromTo(21, 23));
|
||||
addSupportedSigAlg(
|
||||
OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
|
||||
InclusiveIntRange.from(21));
|
||||
}
|
||||
|
||||
public static void addSupportedSigAlg(
|
||||
String digestAlgorithmOid,
|
||||
String signatureAlgorithmOid,
|
||||
InclusiveIntRange... supportedApiLevels) {
|
||||
SUPPORTED_SIG_ALG_OIDS.put(
|
||||
digestAlgorithmOid + "with" + signatureAlgorithmOid,
|
||||
Arrays.asList(supportedApiLevels));
|
||||
}
|
||||
|
||||
public static List<InclusiveIntRange> getSigAlgSupportedApiLevels(
|
||||
String digestAlgorithmOid,
|
||||
String signatureAlgorithmOid) {
|
||||
List<InclusiveIntRange> result =
|
||||
SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
|
||||
return (result != null) ? result : Collections.emptyList();
|
||||
}
|
||||
|
||||
public static class OidToUserFriendlyNameMapper {
|
||||
private OidToUserFriendlyNameMapper() {}
|
||||
|
||||
private static final Map<String, String> OID_TO_USER_FRIENDLY_NAME = new HashMap<>();
|
||||
static {
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA224, "SHA-224");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA256, "SHA-256");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA384, "SHA-384");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA512, "SHA-512");
|
||||
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_RSA, "RSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_MD5_WITH_RSA, "MD5 with RSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_RSA, "SHA-1 with RSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_RSA, "SHA-224 with RSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_RSA, "SHA-256 with RSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_RSA, "SHA-384 with RSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_RSA, "SHA-512 with RSA");
|
||||
|
||||
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_DSA, "DSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_DSA, "SHA-384 with DSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 with DSA");
|
||||
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_ECDSA, "SHA-224 with ECDSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_ECDSA, "SHA-256 with ECDSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_ECDSA, "SHA-384 with ECDSA");
|
||||
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA");
|
||||
}
|
||||
|
||||
public static String getUserFriendlyNameForOid(String oid) {
|
||||
return OID_TO_USER_FRIENDLY_NAME.get(oid);
|
||||
}
|
||||
}
|
||||
|
||||
public static final Map<String, String> OID_TO_JCA_DIGEST_ALG = new HashMap<>();
|
||||
static {
|
||||
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5");
|
||||
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1");
|
||||
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224");
|
||||
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256");
|
||||
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384");
|
||||
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512");
|
||||
}
|
||||
|
||||
public static final Map<String, String> OID_TO_JCA_SIGNATURE_ALG = new HashMap<>();
|
||||
static {
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA");
|
||||
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA");
|
||||
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA");
|
||||
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA");
|
||||
}
|
||||
|
||||
private OidConstants() {}
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.pkcs7;
|
||||
|
||||
import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
|
||||
import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
|
||||
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1;
|
||||
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
|
||||
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_DSA;
|
||||
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY;
|
||||
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA;
|
||||
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA;
|
||||
import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_DIGEST_ALG;
|
||||
import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_SIGNATURE_ALG;
|
||||
|
||||
import com.android.apksig.internal.apk.v1.DigestAlgorithm;
|
||||
import com.android.apksig.internal.asn1.Asn1Class;
|
||||
import com.android.apksig.internal.asn1.Asn1Field;
|
||||
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||
import com.android.apksig.internal.asn1.Asn1Type;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
|
||||
/**
|
||||
* PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652.
|
||||
*/
|
||||
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||
public class AlgorithmIdentifier {
|
||||
|
||||
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||
public String algorithm;
|
||||
|
||||
@Asn1Field(index = 1, type = Asn1Type.ANY, optional = true)
|
||||
public Asn1OpaqueObject parameters;
|
||||
|
||||
public AlgorithmIdentifier() {}
|
||||
|
||||
public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) {
|
||||
this.algorithm = algorithmOid;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
|
||||
* algorithm.
|
||||
*/
|
||||
public static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
|
||||
DigestAlgorithm digestAlgorithm) {
|
||||
switch (digestAlgorithm) {
|
||||
case SHA1:
|
||||
return new AlgorithmIdentifier(OID_DIGEST_SHA1, ASN1_DER_NULL);
|
||||
case SHA256:
|
||||
return new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL);
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
|
||||
* when signing with the specified key and digest algorithm.
|
||||
*/
|
||||
public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
|
||||
PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning)
|
||||
throws InvalidKeyException {
|
||||
String keyAlgorithm = publicKey.getAlgorithm();
|
||||
String jcaDigestPrefixForSigAlg;
|
||||
switch (digestAlgorithm) {
|
||||
case SHA1:
|
||||
jcaDigestPrefixForSigAlg = "SHA1";
|
||||
break;
|
||||
case SHA256:
|
||||
jcaDigestPrefixForSigAlg = "SHA256";
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
|
||||
return Pair.of(
|
||||
jcaDigestPrefixForSigAlg + "withRSA",
|
||||
new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL));
|
||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
AlgorithmIdentifier sigAlgId;
|
||||
switch (digestAlgorithm) {
|
||||
case SHA1:
|
||||
sigAlgId =
|
||||
new AlgorithmIdentifier(OID_SIG_DSA, ASN1_DER_NULL);
|
||||
break;
|
||||
case SHA256:
|
||||
// DSA signatures with SHA-256 in SignedData are accepted by Android API Level
|
||||
// 21 and higher. However, there are two ways to specify their SignedData
|
||||
// SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
|
||||
// dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
|
||||
// the former.
|
||||
sigAlgId =
|
||||
new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
String signingAlgorithmName =
|
||||
jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA");
|
||||
return Pair.of(signingAlgorithmName, sigAlgId);
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
return Pair.of(
|
||||
jcaDigestPrefixForSigAlg + "withECDSA",
|
||||
new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
|
||||
} else {
|
||||
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getJcaSignatureAlgorithm(
|
||||
String digestAlgorithmOid,
|
||||
String signatureAlgorithmOid) throws SignatureException {
|
||||
// First check whether the signature algorithm OID alone is sufficient
|
||||
String result = OID_TO_JCA_SIGNATURE_ALG.get(signatureAlgorithmOid);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Signature algorithm OID alone is insufficient. Need to combine digest algorithm OID
|
||||
// with signature algorithm OID.
|
||||
String suffix;
|
||||
if (OID_SIG_RSA.equals(signatureAlgorithmOid)) {
|
||||
suffix = "RSA";
|
||||
} else if (OID_SIG_DSA.equals(signatureAlgorithmOid)) {
|
||||
suffix = "DSA";
|
||||
} else if (OID_SIG_EC_PUBLIC_KEY.equals(signatureAlgorithmOid)) {
|
||||
suffix = "ECDSA";
|
||||
} else {
|
||||
throw new SignatureException(
|
||||
"Unsupported JCA Signature algorithm"
|
||||
+ " . Digest algorithm: " + digestAlgorithmOid
|
||||
+ ", signature algorithm: " + signatureAlgorithmOid);
|
||||
}
|
||||
String jcaDigestAlg = getJcaDigestAlgorithm(digestAlgorithmOid);
|
||||
// Canonical name for SHA-1 with ... is SHA1with, rather than SHA1. Same for all other
|
||||
// SHA algorithms.
|
||||
if (jcaDigestAlg.startsWith("SHA-")) {
|
||||
jcaDigestAlg = "SHA" + jcaDigestAlg.substring("SHA-".length());
|
||||
}
|
||||
return jcaDigestAlg + "with" + suffix;
|
||||
}
|
||||
|
||||
public static String getJcaDigestAlgorithm(String oid)
|
||||
throws SignatureException {
|
||||
String result = OID_TO_JCA_DIGEST_ALG.get(oid);
|
||||
if (result == null) {
|
||||
throw new SignatureException("Unsupported digest algorithm: " + oid);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.pkcs7;
|
||||
|
||||
import com.android.apksig.internal.asn1.Asn1Class;
|
||||
import com.android.apksig.internal.asn1.Asn1Field;
|
||||
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||
import com.android.apksig.internal.asn1.Asn1Type;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* PKCS #7 {@code Attribute} as specified in RFC 5652.
|
||||
*/
|
||||
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||
public class Attribute {
|
||||
|
||||
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||
public String attrType;
|
||||
|
||||
@Asn1Field(index = 1, type = Asn1Type.SET_OF)
|
||||
public List<Asn1OpaqueObject> attrValues;
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.pkcs7;
|
||||
|
||||
import com.android.apksig.internal.asn1.Asn1Class;
|
||||
import com.android.apksig.internal.asn1.Asn1Field;
|
||||
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||
import com.android.apksig.internal.asn1.Asn1Type;
|
||||
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||
|
||||
/**
|
||||
* PKCS #7 {@code ContentInfo} as specified in RFC 5652.
|
||||
*/
|
||||
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||
public class ContentInfo {
|
||||
|
||||
@Asn1Field(index = 1, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||
public String contentType;
|
||||
|
||||
@Asn1Field(index = 2, type = Asn1Type.ANY, tagging = Asn1Tagging.EXPLICIT, tagNumber = 0)
|
||||
public Asn1OpaqueObject content;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.pkcs7;
|
||||
|
||||
import com.android.apksig.internal.asn1.Asn1Class;
|
||||
import com.android.apksig.internal.asn1.Asn1Field;
|
||||
import com.android.apksig.internal.asn1.Asn1Type;
|
||||
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* PKCS #7 {@code EncapsulatedContentInfo} as specified in RFC 5652.
|
||||
*/
|
||||
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||
public class EncapsulatedContentInfo {
|
||||
|
||||
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||
public String contentType;
|
||||
|
||||
@Asn1Field(
|
||||
index = 1,
|
||||
type = Asn1Type.OCTET_STRING,
|
||||
tagging = Asn1Tagging.EXPLICIT, tagNumber = 0,
|
||||
optional = true)
|
||||
public ByteBuffer content;
|
||||
|
||||
public EncapsulatedContentInfo() {}
|
||||
|
||||
public EncapsulatedContentInfo(String contentTypeOid) {
|
||||
contentType = contentTypeOid;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.pkcs7;
|
||||
|
||||
import com.android.apksig.internal.asn1.Asn1Class;
|
||||
import com.android.apksig.internal.asn1.Asn1Field;
|
||||
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||
import com.android.apksig.internal.asn1.Asn1Type;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* PKCS #7 {@code IssuerAndSerialNumber} as specified in RFC 5652.
|
||||
*/
|
||||
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||
public class IssuerAndSerialNumber {
|
||||
|
||||
@Asn1Field(index = 0, type = Asn1Type.ANY)
|
||||
public Asn1OpaqueObject issuer;
|
||||
|
||||
@Asn1Field(index = 1, type = Asn1Type.INTEGER)
|
||||
public BigInteger certificateSerialNumber;
|
||||
|
||||
public IssuerAndSerialNumber() {}
|
||||
|
||||
public IssuerAndSerialNumber(Asn1OpaqueObject issuer, BigInteger certificateSerialNumber) {
|
||||
this.issuer = issuer;
|
||||
this.certificateSerialNumber = certificateSerialNumber;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.pkcs7;
|
||||
|
||||
/**
|
||||
* Assorted PKCS #7 constants from RFC 5652.
|
||||
*/
|
||||
public abstract class Pkcs7Constants {
|
||||
private Pkcs7Constants() {}
|
||||
|
||||
public static final String OID_DATA = "1.2.840.113549.1.7.1";
|
||||
public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2";
|
||||
public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3";
|
||||
public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.pkcs7;
|
||||
|
||||
/**
|
||||
* Indicates that an error was encountered while decoding a PKCS #7 structure.
|
||||
*/
|
||||
public class Pkcs7DecodingException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public Pkcs7DecodingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public Pkcs7DecodingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.pkcs7;
|
||||
|
||||
import com.android.apksig.internal.asn1.Asn1Class;
|
||||
import com.android.apksig.internal.asn1.Asn1Field;
|
||||
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||
import com.android.apksig.internal.asn1.Asn1Type;
|
||||
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* PKCS #7 {@code SignedData} as specified in RFC 5652.
|
||||
*/
|
||||
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||
public class SignedData {
|
||||
|
||||
@Asn1Field(index = 0, type = Asn1Type.INTEGER)
|
||||
public int version;
|
||||
|
||||
@Asn1Field(index = 1, type = Asn1Type.SET_OF)
|
||||
public List<AlgorithmIdentifier> digestAlgorithms;
|
||||
|
||||
@Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
|
||||
public EncapsulatedContentInfo encapContentInfo;
|
||||
|
||||
@Asn1Field(
|
||||
index = 3,
|
||||
type = Asn1Type.SET_OF,
|
||||
tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
|
||||
optional = true)
|
||||
public List<Asn1OpaqueObject> certificates;
|
||||
|
||||
@Asn1Field(
|
||||
index = 4,
|
||||
type = Asn1Type.SET_OF,
|
||||
tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
|
||||
optional = true)
|
||||
public List<ByteBuffer> crls;
|
||||
|
||||
@Asn1Field(index = 5, type = Asn1Type.SET_OF)
|
||||
public List<SignerInfo> signerInfos;
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.pkcs7;
|
||||
|
||||
import com.android.apksig.internal.asn1.Asn1Class;
|
||||
import com.android.apksig.internal.asn1.Asn1Field;
|
||||
import com.android.apksig.internal.asn1.Asn1Type;
|
||||
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* PKCS #7 {@code SignerIdentifier} as specified in RFC 5652.
|
||||
*/
|
||||
@Asn1Class(type = Asn1Type.CHOICE)
|
||||
public class SignerIdentifier {
|
||||
|
||||
@Asn1Field(type = Asn1Type.SEQUENCE)
|
||||
public IssuerAndSerialNumber issuerAndSerialNumber;
|
||||
|
||||
@Asn1Field(type = Asn1Type.OCTET_STRING, tagging = Asn1Tagging.IMPLICIT, tagNumber = 0)
|
||||
public ByteBuffer subjectKeyIdentifier;
|
||||
|
||||
public SignerIdentifier() {}
|
||||
|
||||
public SignerIdentifier(IssuerAndSerialNumber issuerAndSerialNumber) {
|
||||
this.issuerAndSerialNumber = issuerAndSerialNumber;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.pkcs7;
|
||||
|
||||
import com.android.apksig.internal.asn1.Asn1Class;
|
||||
import com.android.apksig.internal.asn1.Asn1Field;
|
||||
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||
import com.android.apksig.internal.asn1.Asn1Type;
|
||||
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* PKCS #7 {@code SignerInfo} as specified in RFC 5652.
|
||||
*/
|
||||
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||
public class SignerInfo {
|
||||
|
||||
@Asn1Field(index = 0, type = Asn1Type.INTEGER)
|
||||
public int version;
|
||||
|
||||
@Asn1Field(index = 1, type = Asn1Type.CHOICE)
|
||||
public SignerIdentifier sid;
|
||||
|
||||
@Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
|
||||
public AlgorithmIdentifier digestAlgorithm;
|
||||
|
||||
@Asn1Field(
|
||||
index = 3,
|
||||
type = Asn1Type.SET_OF,
|
||||
tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
|
||||
optional = true)
|
||||
public Asn1OpaqueObject signedAttrs;
|
||||
|
||||
@Asn1Field(index = 4, type = Asn1Type.SEQUENCE)
|
||||
public AlgorithmIdentifier signatureAlgorithm;
|
||||
|
||||
@Asn1Field(index = 5, type = Asn1Type.OCTET_STRING)
|
||||
public ByteBuffer signature;
|
||||
|
||||
@Asn1Field(
|
||||
index = 6,
|
||||
type = Asn1Type.SET_OF,
|
||||
tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
|
||||
optional = true)
|
||||
public List<Attribute> unsignedAttrs;
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.util;
|
||||
|
||||
/**
|
||||
* Android SDK version / API Level constants.
|
||||
*/
|
||||
public abstract class AndroidSdkVersion {
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private AndroidSdkVersion() {}
|
||||
|
||||
/** Android 1.0 */
|
||||
public static final int INITIAL_RELEASE = 1;
|
||||
|
||||
/** Android 2.3. */
|
||||
public static final int GINGERBREAD = 9;
|
||||
|
||||
/** Android 3.0 */
|
||||
public static final int HONEYCOMB = 11;
|
||||
|
||||
/** Android 4.3. The revenge of the beans. */
|
||||
public static final int JELLY_BEAN_MR2 = 18;
|
||||
|
||||
/** Android 4.4. KitKat, another tasty treat. */
|
||||
public static final int KITKAT = 19;
|
||||
|
||||
/** Android 5.0. A flat one with beautiful shadows. But still tasty. */
|
||||
public static final int LOLLIPOP = 21;
|
||||
|
||||
/** Android 6.0. M is for Marshmallow! */
|
||||
public static final int M = 23;
|
||||
|
||||
/** Android 7.0. N is for Nougat. */
|
||||
public static final int N = 24;
|
||||
|
||||
/** Android O. */
|
||||
public static final int O = 26;
|
||||
|
||||
/** Android P. */
|
||||
public static final int P = 28;
|
||||
|
||||
/** Android R. */
|
||||
public static final int R = 30;
|
||||
}
|
||||
@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.util;
|
||||
|
||||
import com.android.apksig.util.DataSink;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.ReadableDataSink;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Growable byte array which can be appended to via {@link DataSink} interface and read from via
|
||||
* {@link DataSource} interface.
|
||||
*/
|
||||
public class ByteArrayDataSink implements ReadableDataSink {
|
||||
|
||||
private static final int MAX_READ_CHUNK_SIZE = 65536;
|
||||
|
||||
private byte[] mArray;
|
||||
private int mSize;
|
||||
|
||||
public ByteArrayDataSink() {
|
||||
this(65536);
|
||||
}
|
||||
|
||||
public ByteArrayDataSink(int initialCapacity) {
|
||||
if (initialCapacity < 0) {
|
||||
throw new IllegalArgumentException("initial capacity: " + initialCapacity);
|
||||
}
|
||||
mArray = new byte[initialCapacity];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||
if (offset < 0) {
|
||||
// Must perform this check because System.arraycopy below doesn't perform it when
|
||||
// length == 0
|
||||
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||
}
|
||||
if (offset > buf.length) {
|
||||
// Must perform this check because System.arraycopy below doesn't perform it when
|
||||
// length == 0
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset: " + offset + ", buf.length: " + buf.length);
|
||||
}
|
||||
if (length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureAvailable(length);
|
||||
System.arraycopy(buf, offset, mArray, mSize, length);
|
||||
mSize += length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(ByteBuffer buf) throws IOException {
|
||||
if (!buf.hasRemaining()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (buf.hasArray()) {
|
||||
consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
|
||||
buf.position(buf.limit());
|
||||
return;
|
||||
}
|
||||
|
||||
ensureAvailable(buf.remaining());
|
||||
byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)];
|
||||
while (buf.hasRemaining()) {
|
||||
int chunkSize = Math.min(buf.remaining(), tmp.length);
|
||||
buf.get(tmp, 0, chunkSize);
|
||||
System.arraycopy(tmp, 0, mArray, mSize, chunkSize);
|
||||
mSize += chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureAvailable(int minAvailable) throws IOException {
|
||||
if (minAvailable <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long minCapacity = ((long) mSize) + minAvailable;
|
||||
if (minCapacity <= mArray.length) {
|
||||
return;
|
||||
}
|
||||
if (minCapacity > Integer.MAX_VALUE) {
|
||||
throw new IOException(
|
||||
"Required capacity too large: " + minCapacity + ", max: " + Integer.MAX_VALUE);
|
||||
}
|
||||
int doubleCurrentSize = (int) Math.min(mArray.length * 2L, Integer.MAX_VALUE);
|
||||
int newSize = (int) Math.max(minCapacity, doubleCurrentSize);
|
||||
mArray = Arrays.copyOf(mArray, newSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() {
|
||||
return mSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer(long offset, int size) {
|
||||
checkChunkValid(offset, size);
|
||||
|
||||
// checkChunkValid ensures that it's OK to cast offset to int.
|
||||
return ByteBuffer.wrap(mArray, (int) offset, size).slice();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||
checkChunkValid(offset, size);
|
||||
|
||||
// checkChunkValid ensures that it's OK to cast offset and size to int.
|
||||
sink.consume(mArray, (int) offset, (int) size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||
checkChunkValid(offset, size);
|
||||
|
||||
// checkChunkValid ensures that it's OK to cast offset to int.
|
||||
dest.put(mArray, (int) offset, size);
|
||||
}
|
||||
|
||||
private void checkChunkValid(long offset, long size) {
|
||||
if (offset < 0) {
|
||||
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||
}
|
||||
if (size < 0) {
|
||||
throw new IndexOutOfBoundsException("size: " + size);
|
||||
}
|
||||
if (offset > mSize) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") > source size (" + mSize + ")");
|
||||
}
|
||||
long endOffset = offset + size;
|
||||
if (endOffset < offset) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||
}
|
||||
if (endOffset > mSize) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSource slice(long offset, long size) {
|
||||
checkChunkValid(offset, size);
|
||||
// checkChunkValid ensures that it's OK to cast offset and size to int.
|
||||
return new SliceDataSource((int) offset, (int) size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice of the growable byte array. The slice's offset and size in the array are fixed.
|
||||
*/
|
||||
private class SliceDataSource implements DataSource {
|
||||
private final int mSliceOffset;
|
||||
private final int mSliceSize;
|
||||
|
||||
private SliceDataSource(int offset, int size) {
|
||||
mSliceOffset = offset;
|
||||
mSliceSize = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() {
|
||||
return mSliceSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||
checkChunkValid(offset, size);
|
||||
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
|
||||
sink.consume(mArray, (int) (mSliceOffset + offset), (int) size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
||||
checkChunkValid(offset, size);
|
||||
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||
// that mSliceOffset + offset does not overflow.
|
||||
return ByteBuffer.wrap(mArray, (int) (mSliceOffset + offset), size).slice();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||
checkChunkValid(offset, size);
|
||||
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||
// that mSliceOffset + offset does not overflow.
|
||||
dest.put(mArray, (int) (mSliceOffset + offset), size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSource slice(long offset, long size) {
|
||||
checkChunkValid(offset, size);
|
||||
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
|
||||
return new SliceDataSource((int) (mSliceOffset + offset), (int) size);
|
||||
}
|
||||
|
||||
private void checkChunkValid(long offset, long size) {
|
||||
if (offset < 0) {
|
||||
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||
}
|
||||
if (size < 0) {
|
||||
throw new IndexOutOfBoundsException("size: " + size);
|
||||
}
|
||||
if (offset > mSliceSize) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") > source size (" + mSliceSize + ")");
|
||||
}
|
||||
long endOffset = offset + size;
|
||||
if (endOffset < offset) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||
}
|
||||
if (endOffset > mSliceSize) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") + size (" + size + ") > source size (" + mSliceSize
|
||||
+ ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.util;
|
||||
|
||||
import com.android.apksig.util.DataSink;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* {@link DataSource} backed by a {@link ByteBuffer}.
|
||||
*/
|
||||
public class ByteBufferDataSource implements DataSource {
|
||||
|
||||
private final ByteBuffer mBuffer;
|
||||
private final int mSize;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
|
||||
* buffer between the buffer's position and limit.
|
||||
*/
|
||||
public ByteBufferDataSource(ByteBuffer buffer) {
|
||||
this(buffer, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
|
||||
* buffer between the buffer's position and limit.
|
||||
*/
|
||||
private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) {
|
||||
mBuffer = (sliceRequired) ? buffer.slice() : buffer;
|
||||
mSize = buffer.remaining();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() {
|
||||
return mSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer(long offset, int size) {
|
||||
checkChunkValid(offset, size);
|
||||
|
||||
// checkChunkValid ensures that it's OK to cast offset to int.
|
||||
int chunkPosition = (int) offset;
|
||||
int chunkLimit = chunkPosition + size;
|
||||
// Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
|
||||
// and limit fields, to be more specific). We thus use synchronization around these
|
||||
// state-changing operations to make instances of this class thread-safe.
|
||||
synchronized (mBuffer) {
|
||||
// ByteBuffer.limit(int) and .position(int) check that that the position >= limit
|
||||
// invariant is not broken. Thus, the only way to safely change position and limit
|
||||
// without caring about their current values is to first set position to 0 or set the
|
||||
// limit to capacity.
|
||||
mBuffer.position(0);
|
||||
|
||||
mBuffer.limit(chunkLimit);
|
||||
mBuffer.position(chunkPosition);
|
||||
return mBuffer.slice();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyTo(long offset, int size, ByteBuffer dest) {
|
||||
dest.put(getByteBuffer(offset, size));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||
if ((size < 0) || (size > mSize)) {
|
||||
throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
|
||||
}
|
||||
sink.consume(getByteBuffer(offset, (int) size));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufferDataSource slice(long offset, long size) {
|
||||
if ((offset == 0) && (size == mSize)) {
|
||||
return this;
|
||||
}
|
||||
if ((size < 0) || (size > mSize)) {
|
||||
throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
|
||||
}
|
||||
return new ByteBufferDataSource(
|
||||
getByteBuffer(offset, (int) size),
|
||||
false // no need to slice -- it's already a slice
|
||||
);
|
||||
}
|
||||
|
||||
private void checkChunkValid(long offset, long size) {
|
||||
if (offset < 0) {
|
||||
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||
}
|
||||
if (size < 0) {
|
||||
throw new IndexOutOfBoundsException("size: " + size);
|
||||
}
|
||||
if (offset > mSize) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") > source size (" + mSize + ")");
|
||||
}
|
||||
long endOffset = offset + size;
|
||||
if (endOffset < offset) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||
}
|
||||
if (endOffset > mSize) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.util;
|
||||
|
||||
import com.android.apksig.util.DataSink;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferOverflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Data sink which stores all received data into the associated {@link ByteBuffer}.
|
||||
*/
|
||||
public class ByteBufferSink implements DataSink {
|
||||
|
||||
private final ByteBuffer mBuffer;
|
||||
|
||||
public ByteBufferSink(ByteBuffer buffer) {
|
||||
mBuffer = buffer;
|
||||
}
|
||||
|
||||
public ByteBuffer getBuffer() {
|
||||
return mBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||
try {
|
||||
mBuffer.put(buf, offset, length);
|
||||
} catch (BufferOverflowException e) {
|
||||
throw new IOException(
|
||||
"Insufficient space in output buffer for " + length + " bytes", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(ByteBuffer buf) throws IOException {
|
||||
int length = buf.remaining();
|
||||
try {
|
||||
mBuffer.put(buf);
|
||||
} catch (BufferOverflowException e) {
|
||||
throw new IOException(
|
||||
"Insufficient space in output buffer for " + length + " bytes", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.util;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class ByteBufferUtils {
|
||||
private ByteBufferUtils() {}
|
||||
|
||||
/**
|
||||
* Returns the remaining data of the provided buffer as a new byte array and advances the
|
||||
* position of the buffer to the buffer's limit.
|
||||
*/
|
||||
public static byte[] toByteArray(ByteBuffer buf) {
|
||||
byte[] result = new byte[buf.remaining()];
|
||||
buf.get(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Utilities for byte arrays and I/O streams.
|
||||
*/
|
||||
public final class ByteStreams {
|
||||
private ByteStreams() {}
|
||||
|
||||
/**
|
||||
* Returns the data remaining in the provided input stream as a byte array
|
||||
*/
|
||||
public static byte[] toByteArray(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[16384];
|
||||
int chunkSize;
|
||||
while ((chunkSize = in.read(buf)) != -1) {
|
||||
result.write(buf, 0, chunkSize);
|
||||
}
|
||||
return result.toByteArray();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.util;
|
||||
|
||||
import com.android.apksig.util.DataSink;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Pseudo {@link DataSource} that chains the given {@link DataSource} as a continuous one. */
|
||||
public class ChainedDataSource implements DataSource {
|
||||
|
||||
private final DataSource[] mSources;
|
||||
private final long mTotalSize;
|
||||
|
||||
public ChainedDataSource(DataSource... sources) {
|
||||
mSources = sources;
|
||||
mTotalSize = Arrays.stream(sources).mapToLong(src -> src.size()).sum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() {
|
||||
return mTotalSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||
if (offset + size > mTotalSize) {
|
||||
throw new IndexOutOfBoundsException("Requested more than available");
|
||||
}
|
||||
|
||||
for (DataSource src : mSources) {
|
||||
// Offset is beyond the current source. Skip.
|
||||
if (offset >= src.size()) {
|
||||
offset -= src.size();
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the remaining is enough, finish it.
|
||||
long remaining = src.size() - offset;
|
||||
if (remaining >= size) {
|
||||
src.feed(offset, size, sink);
|
||||
break;
|
||||
}
|
||||
|
||||
// If the remaining is not enough, consume all.
|
||||
src.feed(offset, remaining, sink);
|
||||
size -= remaining;
|
||||
offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
||||
if (offset + size > mTotalSize) {
|
||||
throw new IndexOutOfBoundsException("Requested more than available");
|
||||
}
|
||||
|
||||
// Skip to the first DataSource we need.
|
||||
Pair<Integer, Long> firstSource = locateDataSource(offset);
|
||||
int i = firstSource.getFirst();
|
||||
offset = firstSource.getSecond();
|
||||
|
||||
// Return the current source's ByteBuffer if it fits.
|
||||
if (offset + size <= mSources[i].size()) {
|
||||
return mSources[i].getByteBuffer(offset, size);
|
||||
}
|
||||
|
||||
// Otherwise, read into a new buffer.
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||
for (; i < mSources.length && buffer.hasRemaining(); i++) {
|
||||
long sizeToCopy = Math.min(mSources[i].size() - offset, buffer.remaining());
|
||||
mSources[i].copyTo(offset, Math.toIntExact(sizeToCopy), buffer);
|
||||
offset = 0; // may not be zero for the first source, but reset after that.
|
||||
}
|
||||
buffer.rewind();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||
feed(offset, size, new ByteBufferSink(dest));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSource slice(long offset, long size) {
|
||||
// Find the first slice.
|
||||
Pair<Integer, Long> firstSource = locateDataSource(offset);
|
||||
int beginIndex = firstSource.getFirst();
|
||||
long beginLocalOffset = firstSource.getSecond();
|
||||
DataSource beginSource = mSources[beginIndex];
|
||||
|
||||
if (beginLocalOffset + size <= beginSource.size()) {
|
||||
return beginSource.slice(beginLocalOffset, size);
|
||||
}
|
||||
|
||||
// Add the first slice to chaining, followed by the middle full slices, then the last.
|
||||
ArrayList<DataSource> sources = new ArrayList<>();
|
||||
sources.add(beginSource.slice(
|
||||
beginLocalOffset, beginSource.size() - beginLocalOffset));
|
||||
|
||||
Pair<Integer, Long> lastSource = locateDataSource(offset + size - 1);
|
||||
int endIndex = lastSource.getFirst();
|
||||
long endLocalOffset = lastSource.getSecond();
|
||||
|
||||
for (int i = beginIndex + 1; i < endIndex; i++) {
|
||||
sources.add(mSources[i]);
|
||||
}
|
||||
|
||||
sources.add(mSources[endIndex].slice(0, endLocalOffset + 1));
|
||||
return new ChainedDataSource(sources.toArray(new DataSource[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the index of DataSource that offset is at.
|
||||
* @return Pair of DataSource index and the local offset in the DataSource.
|
||||
*/
|
||||
private Pair<Integer, Long> locateDataSource(long offset) {
|
||||
long localOffset = offset;
|
||||
for (int i = 0; i < mSources.length; i++) {
|
||||
if (localOffset < mSources[i].size()) {
|
||||
return Pair.of(i, localOffset);
|
||||
}
|
||||
localOffset -= mSources[i].size();
|
||||
}
|
||||
throw new IndexOutOfBoundsException("Access is out of bound, offset: " + offset +
|
||||
", totalSize: " + mTotalSize);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.Principal;
|
||||
import java.security.Provider;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
/**
|
||||
* {@link X509Certificate} which delegates all method invocations to the provided delegate
|
||||
* {@code X509Certificate}.
|
||||
*/
|
||||
public class DelegatingX509Certificate extends X509Certificate {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final X509Certificate mDelegate;
|
||||
|
||||
public DelegatingX509Certificate(X509Certificate delegate) {
|
||||
this.mDelegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getCriticalExtensionOIDs() {
|
||||
return mDelegate.getCriticalExtensionOIDs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getExtensionValue(String oid) {
|
||||
return mDelegate.getExtensionValue(oid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNonCriticalExtensionOIDs() {
|
||||
return mDelegate.getNonCriticalExtensionOIDs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasUnsupportedCriticalExtension() {
|
||||
return mDelegate.hasUnsupportedCriticalExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkValidity()
|
||||
throws CertificateExpiredException, CertificateNotYetValidException {
|
||||
mDelegate.checkValidity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkValidity(Date date)
|
||||
throws CertificateExpiredException, CertificateNotYetValidException {
|
||||
mDelegate.checkValidity(date);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return mDelegate.getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getSerialNumber() {
|
||||
return mDelegate.getSerialNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal getIssuerDN() {
|
||||
return mDelegate.getIssuerDN();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal getSubjectDN() {
|
||||
return mDelegate.getSubjectDN();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getNotBefore() {
|
||||
return mDelegate.getNotBefore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getNotAfter() {
|
||||
return mDelegate.getNotAfter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getTBSCertificate() throws CertificateEncodingException {
|
||||
return mDelegate.getTBSCertificate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSignature() {
|
||||
return mDelegate.getSignature();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSigAlgName() {
|
||||
return mDelegate.getSigAlgName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSigAlgOID() {
|
||||
return mDelegate.getSigAlgOID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSigAlgParams() {
|
||||
return mDelegate.getSigAlgParams();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] getIssuerUniqueID() {
|
||||
return mDelegate.getIssuerUniqueID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] getSubjectUniqueID() {
|
||||
return mDelegate.getSubjectUniqueID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] getKeyUsage() {
|
||||
return mDelegate.getKeyUsage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBasicConstraints() {
|
||||
return mDelegate.getBasicConstraints();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() throws CertificateEncodingException {
|
||||
return mDelegate.getEncoded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
|
||||
InvalidKeyException, NoSuchProviderException, SignatureException {
|
||||
mDelegate.verify(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(PublicKey key, String sigProvider)
|
||||
throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
|
||||
NoSuchProviderException, SignatureException {
|
||||
mDelegate.verify(key, sigProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mDelegate.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getPublicKey() {
|
||||
return mDelegate.getPublicKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public X500Principal getIssuerX500Principal() {
|
||||
return mDelegate.getIssuerX500Principal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public X500Principal getSubjectX500Principal() {
|
||||
return mDelegate.getSubjectX500Principal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getExtendedKeyUsage() throws CertificateParsingException {
|
||||
return mDelegate.getExtendedKeyUsage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException {
|
||||
return mDelegate.getSubjectAlternativeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<List<?>> getIssuerAlternativeNames() throws CertificateParsingException {
|
||||
return mDelegate.getIssuerAlternativeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(PublicKey key, Provider sigProvider) throws CertificateException,
|
||||
NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||
mDelegate.verify(key, sigProvider);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.util;
|
||||
|
||||
import com.android.apksig.util.DataSink;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.BufferOverflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
/**
|
||||
* {@link DataSource} backed by a {@link FileChannel} for {@link RandomAccessFile} access.
|
||||
*/
|
||||
public class FileChannelDataSource implements DataSource {
|
||||
|
||||
private static final int MAX_READ_CHUNK_SIZE = 1024 * 1024;
|
||||
|
||||
private final FileChannel mChannel;
|
||||
private final long mOffset;
|
||||
private final long mSize;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code FileChannelDataSource} based on the data contained in the
|
||||
* whole file. Changes to the contents of the file, including the size of the file,
|
||||
* will be visible in this data source.
|
||||
*/
|
||||
public FileChannelDataSource(FileChannel channel) {
|
||||
mChannel = channel;
|
||||
mOffset = 0;
|
||||
mSize = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code FileChannelDataSource} based on the data contained in the
|
||||
* specified region of the provided file. Changes to the contents of the file will be visible in
|
||||
* this data source.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative.
|
||||
*/
|
||||
public FileChannelDataSource(FileChannel channel, long offset, long size) {
|
||||
if (offset < 0) {
|
||||
throw new IndexOutOfBoundsException("offset: " + size);
|
||||
}
|
||||
if (size < 0) {
|
||||
throw new IndexOutOfBoundsException("size: " + size);
|
||||
}
|
||||
mChannel = channel;
|
||||
mOffset = offset;
|
||||
mSize = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() {
|
||||
if (mSize == -1) {
|
||||
try {
|
||||
return mChannel.size();
|
||||
} catch (IOException e) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return mSize;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileChannelDataSource slice(long offset, long size) {
|
||||
long sourceSize = size();
|
||||
checkChunkValid(offset, size, sourceSize);
|
||||
if ((offset == 0) && (size == sourceSize)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return new FileChannelDataSource(mChannel, mOffset + offset, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||
long sourceSize = size();
|
||||
checkChunkValid(offset, size, sourceSize);
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long chunkOffsetInFile = mOffset + offset;
|
||||
long remaining = size;
|
||||
ByteBuffer buf = ByteBuffer.allocateDirect((int) Math.min(remaining, MAX_READ_CHUNK_SIZE));
|
||||
|
||||
while (remaining > 0) {
|
||||
int chunkSize = (int) Math.min(remaining, buf.capacity());
|
||||
int chunkRemaining = chunkSize;
|
||||
buf.limit(chunkSize);
|
||||
synchronized (mChannel) {
|
||||
mChannel.position(chunkOffsetInFile);
|
||||
while (chunkRemaining > 0) {
|
||||
int read = mChannel.read(buf);
|
||||
if (read < 0) {
|
||||
throw new IOException("Unexpected EOF encountered");
|
||||
}
|
||||
chunkRemaining -= read;
|
||||
}
|
||||
}
|
||||
buf.flip();
|
||||
sink.consume(buf);
|
||||
buf.clear();
|
||||
chunkOffsetInFile += chunkSize;
|
||||
remaining -= chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||
long sourceSize = size();
|
||||
checkChunkValid(offset, size, sourceSize);
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
if (size > dest.remaining()) {
|
||||
throw new BufferOverflowException();
|
||||
}
|
||||
|
||||
long offsetInFile = mOffset + offset;
|
||||
int remaining = size;
|
||||
int prevLimit = dest.limit();
|
||||
try {
|
||||
// FileChannel.read(ByteBuffer) reads up to dest.remaining(). Thus, we need to adjust
|
||||
// the buffer's limit to avoid reading more than size bytes.
|
||||
dest.limit(dest.position() + size);
|
||||
while (remaining > 0) {
|
||||
int chunkSize;
|
||||
synchronized (mChannel) {
|
||||
mChannel.position(offsetInFile);
|
||||
chunkSize = mChannel.read(dest);
|
||||
}
|
||||
offsetInFile += chunkSize;
|
||||
remaining -= chunkSize;
|
||||
}
|
||||
} finally {
|
||||
dest.limit(prevLimit);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
||||
if (size < 0) {
|
||||
throw new IndexOutOfBoundsException("size: " + size);
|
||||
}
|
||||
ByteBuffer result = ByteBuffer.allocate(size);
|
||||
copyTo(offset, size, result);
|
||||
result.flip();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void checkChunkValid(long offset, long size, long sourceSize) {
|
||||
if (offset < 0) {
|
||||
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||
}
|
||||
if (size < 0) {
|
||||
throw new IndexOutOfBoundsException("size: " + size);
|
||||
}
|
||||
if (offset > sourceSize) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") > source size (" + sourceSize + ")");
|
||||
}
|
||||
long endOffset = offset + size;
|
||||
if (endOffset < offset) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||
}
|
||||
if (endOffset > sourceSize) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"offset (" + offset + ") + size (" + size
|
||||
+ ") > source size (" + sourceSize +")");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user