From 20ec03caa21cedf1158ab1184afeb6a91c72d73e Mon Sep 17 00:00:00 2001 From: ZaneYork Date: Mon, 23 Mar 2020 23:36:12 +0800 Subject: [PATCH] 1.New Apksigner (to deal with large apk file) 2.Compat for Galaxy Store Stardew Valley --- app/build.gradle | 4 +- app/proguard-rules.pro | 1 + app/src/main/assets/apk/StardewModdingAPI.dll | Bin 584192 -> 583680 bytes app/src/main/assets/apk_files_manifest.json | 5 + .../assets/downloadable_content_list.json | 10 +- .../assets/downloadable_content_list.json.en | 10 +- .../assets/downloadable_content_list.json.zh | 12 +- app/src/main/assets/package_names.json | 1 + .../java/com/android/apksig/ApkSigner.java | 1302 ++++++++++ .../com/android/apksig/ApkSignerEngine.java | 520 ++++ .../java/com/android/apksig/ApkVerifier.java | 1909 +++++++++++++++ .../apksig/DefaultApkSignerEngine.java | 1506 ++++++++++++ .../main/java/com/android/apksig/Hints.java | 82 + .../apksig/SigningCertificateLineage.java | 1086 +++++++++ .../apksig/apk/ApkFormatException.java | 35 + .../apk/ApkSigningBlockNotFoundException.java | 32 + .../java/com/android/apksig/apk/ApkUtils.java | 604 +++++ .../apk/CodenameMinSdkVersionException.java | 46 + .../apksig/apk/MinSdkVersionException.java | 40 + .../internal/apk/AndroidBinXmlParser.java | 869 +++++++ .../internal/apk/ApkSigningBlockUtils.java | 1356 +++++++++++ .../internal/apk/ContentDigestAlgorithm.java | 55 + .../internal/apk/SignatureAlgorithm.java | 193 ++ .../apksig/internal/apk/SignatureInfo.java | 53 + .../internal/apk/v1/DigestAlgorithm.java | 74 + .../internal/apk/v1/V1SchemeSigner.java | 688 ++++++ .../internal/apk/v1/V1SchemeVerifier.java | 2099 +++++++++++++++++ .../internal/apk/v2/V2SchemeSigner.java | 294 +++ .../internal/apk/v2/V2SchemeVerifier.java | 462 ++++ .../internal/apk/v3/V3SchemeSigner.java | 325 +++ .../internal/apk/v3/V3SchemeVerifier.java | 518 ++++ .../apk/v3/V3SigningCertificateLineage.java | 306 +++ .../apksig/internal/asn1/Asn1BerParser.java | 674 ++++++ .../apksig/internal/asn1/Asn1Class.java | 28 + .../internal/asn1/Asn1DecodingException.java | 32 + .../apksig/internal/asn1/Asn1DerEncoder.java | 593 +++++ .../internal/asn1/Asn1EncodingException.java | 32 + .../apksig/internal/asn1/Asn1Field.java | 45 + .../internal/asn1/Asn1OpaqueObject.java | 38 + .../apksig/internal/asn1/Asn1TagClass.java | 30 + .../apksig/internal/asn1/Asn1Tagging.java | 23 + .../apksig/internal/asn1/Asn1Type.java | 35 + .../internal/asn1/ber/BerDataValue.java | 115 + .../asn1/ber/BerDataValueFormatException.java | 34 + .../internal/asn1/ber/BerDataValueReader.java | 34 + .../apksig/internal/asn1/ber/BerEncoding.java | 225 ++ .../ber/ByteBufferBerDataValueReader.java | 208 ++ .../ber/InputStreamBerDataValueReader.java | 313 +++ .../apksig/internal/jar/ManifestParser.java | 363 +++ .../apksig/internal/jar/ManifestWriter.java | 127 + .../internal/jar/SignatureFileWriter.java | 61 + .../internal/pkcs7/AlgorithmIdentifier.java | 42 + .../apksig/internal/pkcs7/Attribute.java | 36 + .../apksig/internal/pkcs7/ContentInfo.java | 36 + .../pkcs7/EncapsulatedContentInfo.java | 46 + .../internal/pkcs7/IssuerAndSerialNumber.java | 43 + .../apksig/internal/pkcs7/Pkcs7Constants.java | 29 + .../pkcs7/Pkcs7DecodingException.java | 32 + .../apksig/internal/pkcs7/SignedData.java | 58 + .../internal/pkcs7/SignerIdentifier.java | 42 + .../apksig/internal/pkcs7/SignerInfo.java | 61 + .../internal/util/AndroidSdkVersion.java | 50 + .../internal/util/ByteArrayDataSink.java | 240 ++ .../internal/util/ByteBufferDataSource.java | 125 + .../apksig/internal/util/ByteBufferSink.java | 59 + .../apksig/internal/util/ByteBufferUtils.java | 33 + .../apksig/internal/util/ByteStreams.java | 41 + .../internal/util/ChainedDataSource.java | 145 ++ .../util/DelegatingX509Certificate.java | 217 ++ .../GuaranteedEncodedFormX509Certificate.java | 68 + .../internal/util/InclusiveIntRange.java | 89 + .../internal/util/MessageDigestSink.java | 51 + .../internal/util/OutputStreamDataSink.java | 77 + .../android/apksig/internal/util/Pair.java | 81 + .../util/RandomAccessFileDataSink.java | 104 + .../util/RandomAccessFileDataSource.java | 195 ++ .../apksig/internal/util/TeeDataSink.java | 51 + .../internal/util/VerityTreeBuilder.java | 216 ++ .../internal/util/X509CertificateUtils.java | 278 +++ .../internal/x509/AttributeTypeAndValue.java | 35 + .../apksig/internal/x509/Certificate.java | 39 + .../apksig/internal/x509/Extension.java | 38 + .../android/apksig/internal/x509/Name.java | 34 + .../x509/RelativeDistinguishedName.java | 33 + .../internal/x509/SubjectPublicKeyInfo.java | 36 + .../apksig/internal/x509/TBSCertificate.java | 79 + .../android/apksig/internal/x509/Time.java | 34 + .../apksig/internal/x509/Validity.java | 34 + .../internal/zip/CentralDirectoryRecord.java | 304 +++ .../apksig/internal/zip/EocdRecord.java | 48 + .../apksig/internal/zip/LocalFileRecord.java | 537 +++++ .../android/apksig/internal/zip/ZipUtils.java | 325 +++ .../com/android/apksig/util/DataSink.java | 46 + .../com/android/apksig/util/DataSinks.java | 75 + .../com/android/apksig/util/DataSource.java | 110 + .../com/android/apksig/util/DataSources.java | 63 + .../android/apksig/util/ReadableDataSink.java | 24 + .../apksig/util/RunnablesExecutor.java | 23 + .../apksig/util/RunnablesProvider.java | 21 + .../apksig/zip/ZipFormatException.java | 32 + .../zane/smapiinstaller/MainApplication.java | 2 + .../entity/ApkFilesManifest.java | 3 + .../zane/smapiinstaller/logic/ApkPatcher.java | 47 +- .../smapiinstaller/logic/CommonLogic.java | 10 +- .../java/net/fornwall/apksigner/Base64.java | 21 - .../net/fornwall/apksigner/CertCreator.java | 158 -- .../java/net/fornwall/apksigner/KeySet.java | 20 - .../apksigner/SignatureBlockGenerator.java | 56 - .../net/fornwall/apksigner/ZipAligner.java | 29 + .../net/fornwall/apksigner/ZipSigner.java | 186 -- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-ko-rKR/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-th/strings.xml | 2 +- app/src/main/res/values-zh-rHK/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 120 files changed, 22101 insertions(+), 473 deletions(-) create mode 100644 app/src/main/java/com/android/apksig/ApkSigner.java create mode 100644 app/src/main/java/com/android/apksig/ApkSignerEngine.java create mode 100644 app/src/main/java/com/android/apksig/ApkVerifier.java create mode 100644 app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java create mode 100644 app/src/main/java/com/android/apksig/Hints.java create mode 100644 app/src/main/java/com/android/apksig/SigningCertificateLineage.java create mode 100644 app/src/main/java/com/android/apksig/apk/ApkFormatException.java create mode 100644 app/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java create mode 100644 app/src/main/java/com/android/apksig/apk/ApkUtils.java create mode 100644 app/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java create mode 100644 app/src/main/java/com/android/apksig/apk/MinSdkVersionException.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java create mode 100644 app/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java create mode 100644 app/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java create mode 100644 app/src/main/java/com/android/apksig/internal/jar/ManifestParser.java create mode 100644 app/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java create mode 100644 app/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java create mode 100644 app/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java create mode 100644 app/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java create mode 100644 app/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java create mode 100644 app/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java create mode 100644 app/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java create mode 100644 app/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java create mode 100644 app/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java create mode 100644 app/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java create mode 100644 app/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java create mode 100644 app/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/ByteStreams.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/Pair.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSource.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/TeeDataSink.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java create mode 100644 app/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java create mode 100644 app/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java create mode 100644 app/src/main/java/com/android/apksig/internal/x509/Certificate.java create mode 100644 app/src/main/java/com/android/apksig/internal/x509/Extension.java create mode 100644 app/src/main/java/com/android/apksig/internal/x509/Name.java create mode 100644 app/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java create mode 100644 app/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java create mode 100644 app/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java create mode 100644 app/src/main/java/com/android/apksig/internal/x509/Time.java create mode 100644 app/src/main/java/com/android/apksig/internal/x509/Validity.java create mode 100644 app/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java create mode 100644 app/src/main/java/com/android/apksig/internal/zip/EocdRecord.java create mode 100644 app/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java create mode 100644 app/src/main/java/com/android/apksig/internal/zip/ZipUtils.java create mode 100644 app/src/main/java/com/android/apksig/util/DataSink.java create mode 100644 app/src/main/java/com/android/apksig/util/DataSinks.java create mode 100644 app/src/main/java/com/android/apksig/util/DataSource.java create mode 100644 app/src/main/java/com/android/apksig/util/DataSources.java create mode 100644 app/src/main/java/com/android/apksig/util/ReadableDataSink.java create mode 100644 app/src/main/java/com/android/apksig/util/RunnablesExecutor.java create mode 100644 app/src/main/java/com/android/apksig/util/RunnablesProvider.java create mode 100644 app/src/main/java/com/android/apksig/zip/ZipFormatException.java delete mode 100644 app/src/main/java/net/fornwall/apksigner/Base64.java delete mode 100644 app/src/main/java/net/fornwall/apksigner/CertCreator.java delete mode 100644 app/src/main/java/net/fornwall/apksigner/KeySet.java delete mode 100644 app/src/main/java/net/fornwall/apksigner/SignatureBlockGenerator.java create mode 100644 app/src/main/java/net/fornwall/apksigner/ZipAligner.java delete mode 100644 app/src/main/java/net/fornwall/apksigner/ZipSigner.java diff --git a/app/build.gradle b/app/build.gradle index fbfd71c..1b8939c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.zane.smapiinstaller" minSdkVersion 19 targetSdkVersion 28 - versionCode 20 - versionName "1.3.5" + versionCode 21 + versionName "1.3.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index b46250f..8ff6aed 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -138,6 +138,7 @@ -keep class com.zane.** { *; } -keep class pxb.android.** { *; } -keep class net.fornwall.apksigner.** { *; } +-keep class com.android.apksig.** { *; } -keep class org.spongycastle.** -dontwarn org.spongycastle.jce.provider.X509LDAPCertStoreSpi -dontwarn org.spongycastle.x509.util.LDAPStoreHelper diff --git a/app/src/main/assets/apk/StardewModdingAPI.dll b/app/src/main/assets/apk/StardewModdingAPI.dll index aa782dd7dab5fafc36c6629cac903c05d128e56d..9134e0768623b1f2cc966420e4c2299075f91bd2 100644 GIT binary patch literal 583680 zcmcG%31Az=^*=sd$tzj19k1m`w&TRkVY66vfIz^Ba)&G22@sNS1rh=YkPWK@XcP%U zxk)Hc&TxlP?sAp890f|bNx4c31j=2Y2ebvsVSb;_o86V=bv7nYg_NHRllryZuq?wyFE92pCcA$+m^Y` z;qDTPpQ-R4G$KlV2j<#&{Kf>LhMxxe1{B@;3!|!1)(aQ%)f%|1XB#BSH9k z?2D+{Q({%>bSk3%Ut5)`25jelMO9CK+<$s*QwkCXe^ET|96TvCdii0;FURvgH+bb@ zSv|RZG9;Nl+ReHP0MN3bY%J(IrJSX%ri^wEJIVneWmS+_czv)D-87<0|Gq@|`L{Fv z8LCok9}B6IKd4j{0#)kCzeiPXpj;35AA(^9Q((%3|fzB>qyRJQ@%UoS=)Q9>89}7g)YW^ZAgPQt~_v|Gl zP$#u_1q14AH!s|sVNrJkLw5o#>JH`#-5m!mCAuT2DSXOx$FydSza_fc>61$yDAOHN z51>0qwh7%$F4G+u{6O6?lmA9{dmue^h~`|XJA$D*ffjWKlZNh&FV`JO%^ZKrb;q=( z@Ga3@Hni_eWxA7e8`~WdZ_4h@D$^Yq{6O6?lmA9{dm%k_HxOnd5J{?wHmTz9qW*pzZvt%XBB{Hr5>zZ%TKslL zJA$D*ffjX#?hJOfvRrp0HFNwe*B#TE!nZ_s|9*A<`^$7E={D9K6K_g)Q!7gS9U1&U z-7%B@MtA!lJ$1J)!=mm8hVBGf)Lle$mn+vDNzEL8%XP=JrtmG%-I!U)>&tW}={D9K z6K_g)=a%V?41S>Qn8|;myV*!j-R;M)s5^q8JAoE;7Zu&DD%TxJ%^ZKrb;q=(@Ga5Z zw2NvNl<7{=ZLB*c-jwdXFVh_v{6O6?lmA9{bC908o6E4MJA$D*ffjX#(G=QjzFc=C zHFNwe*B#TE!nZ_sM{oC0MVam--Nw3O;!Wvpx>4F@$>0a-j+y*7x|@ge)ZP9Ji@GBi zx)W$ocQMgjpf4s!Vs1Ze!gs@uqZlTAA+1;0NlCnfy1p zI{@jay8{^(bw@CCC(xqq;-b3~%5_IlGsoX@-7&2xd`ooKcf*46F^Z(ySa(dkDcyZn zraLnDfx2TR|Bdc2KNQ^^%&@3Cf}uNs7IjxGy6Y|19ZAg`f6H~pv}TUKCAxcP-prY0 zb|>jJ)}18V#MpT2P-&YbgCD3nX7Yn|*IY++u;FX{gO$wv8rjHjR$B2O?d%()|$v6mN4PX%mJz4g>xg*n~2iLr{Qt zBxa5~1TfQnC!);iXxmp09{t#~*A!;$ZNmI$NgkR234uj zK(0IfC%a@a-UBHz$6yZGGZ?(0ZIch2dQxULJd^TIg>6pByo#wMAIMWveuA{ow#{yt zzJ|PSUo`df%=TW2^KY8II&+_wf)trweGVCD%D zoi9Y6{`JkPGBZ4OEK;U|$m0b0hXY#vI(B)q?WYG`^eXc{>*1px&peAFQ$TgVMXzMu z_t=sm^B1O^C@Jrp+wv*%ZkyBcN#+QTJ%O1W@zB#KL_6-^@|Vn)9tY-?d4(Ll0Y$X! zyhB<(W1dGH)bcmc1M~d;;FiB<_Vw72BC{>oJtZkVSg_T<$2LdX+84BZo~iPP2pqkv z=Q^SI(P3MCJ$er^8na}p??!LYa}L0!dl7BtP*n2xMm+cA1b9sV3=z0UfVT#~Zz$X9AZ9aK8XJg}{9UxF7(g z2_(jJod7tJKw?bygaBAeU{Zi*1;Bs9s(NU8sEz>m9)X|Y$8@g?fG-gEdjZ}P0PiR8 zQ2{;{0IwzR76HBx0M8-tVgbJH1G@`7rwZi50CH6NIMe+{@OWVQ2={wDWvvQj)}#-1 z<7V(M%WWz??BH%zeCTnvEDTPao7NxJPl)FA=u=9CXdkkXSm~tu z3S=-=qwA}%4})5=i}173FeBN&hn1NKrl*SKQaBh1kt!kCzO3;5p$FP-9%I^GaLHMqw$yp^O%i#Pup-D z>FhChjI3VF(z%Fgvyr}QOUP3v@~}^ifq^g45PKIuOzD&35j%@?-JynbMMP4w-}@8N z^3NTKtdrU0py=gjz}*7R*)L%Jy&M%Z^m0&OuK?OP7C&wVXkpvSF+vG~b*d2@HP{#r zneK{!m_rDG^>P$Zn2w*`HvG6}fTVXgV=pl_0x=tFHm3V0rag{gjU@JO#Ev2sE4-$Q zRevuxjsgnDgH8+0?myYf-apw3^XBNLn*jz05;hk`dyf)6WTP#ZT^;t1z@e+vUWuT) zv5;e66;=@#vGWMh6)7$&&$eM7W>>YY!-60Mp@X5Xq6He2xh~4X@ zSz0&|v95Z_as}+9qM62FgKCUBe0e(5lkZ{ z`2dLkFxdx4B-YTmP8s%xD9Q>*T+!Yg>PVXoDlVpsl=FAMfrh!qR44u)0(%(@ThV_; zQWI>58g`N-Z?Jt-)N#$cLdC1~_QnXkzORTIf;IiM2rc>F{xK54*8hcEvL_?%oI)=4 zKbfoAaNG)%3KE4olIdj3bWR0QhHuh!$0N{ZpVgu4uRvbyR#d5FQBxX*`)v$10TE19 zNL8q?a~j#I4IMSgjmFioDrw)FqN1P`bd>mS_>QU@hKN7Jv#Mh&M7S?86_9#qZ*!43 zcNI+MbYy~6_Ace_j6CcB@t;KUb%L}iAvy^;XMj)kOfpl_IQv$lXD)THC0`MJj_q=m(X)-<7J+G!zzY+UFFw+t zkhsgz%o%qs0vc@zF1p$Mver*ZlvIr`9DN~*tzgNJ?P4T~^`9`r(<&lu;0n;eM`Etv z&Z+~)5ZrV3Aa^O_ux}VOTW?G?R2%Mj1cpJ7np0Di8WT6pUR>1|*6mxmiLg<%wmNEG zhIENas}ef9(dXlV;dy35+;m)AUR0cGTt&?6-UfOm(=wfX5K$XyEVC<$CuzQHjJhqJ zs$%DGYNOOj)mm*|j*QT#Q3kZNEBTwUzX5wxP-4pp1;43n+kiF4-KQyw(6%nLuaY!Z z=NkKJ4Yz{8lgeE4=Y%pYJ|?R-(WfwQ)XdROoiLKQyqmIB7Nrf z0B3&&`u1L8vezP-Ux}Es;q1@x7_zU!ui;#e-=uQ`e%(e3me*%~%fvSV;M~NYoADFZ zZT}WTbqC!D+y4cgl(KJ?U_)gGyn;fxsVAjQQnA&l5_XL?UZEm@G)W!D(wN{%KT9PF ztZhBe#vF~IU00~Fz%>hf?l!Q8@}g}Y!7$?KHY0aCATYpa+n)teYvk?##K!p76r(~~ z+Fu$WD$i2uCqN1rei^It?C|gOSgMe*Pb!{{gp&ItbxQi9?Sgdh`(8Q`ac)`aTBO$C z0eH*<57<9!x?b5~3Kr?IRLIQ!5?P-(34Bx4>G6h?X>`nxlF-ak@G^~L%)<6nWbY~= zk7JjUDU;P0$V&SUDLkv8$R~^L0YeodpGO@lGsmE9{Z82U9$L5?JaYHo2US}Evh2Nx z+m&cU$j?(4%S~Rx$jk6}6*MmfZR^oOeO~u#UZG9$GEi{e?i)v&MoNk%^*hGMRk0U& z06aSVhhLRGJXrehQ0c?NeAv+lg)5V_;o3^Oj-8U~JctkiYR&8;Ah8=Xe6$ft+Vwg< z4Ky+u^O2K`Q1;g)#M8WvRiV45fTG`ul5%LVA4QU)of>T0s#WS)@FC~1A^~lXc>IGM zczEg9qzQ>r z=yb<^8MKbtie;1qpQ4U7*5Q*u4!WI=L>|K>J=)4sK%%YGu&uBjXQG`%LJc#u(a$S9 z4ZQOVep(ym9ce#{$o`8vJ@9h`x6KW}e>Aes1C*~ueHzI;%!L8wpbTIbVZ(j_c=(>) z7@t!4ID*c^mT|QNGRdEjOd9qG2vW@1=Wp1Q7|&<)w&QJfm)6>^8c z**dV_bbb(Xr7j}sU}P#j%gDXN6nzzNeG$Wc84s`_O#Z`wK|!O9*1BZtl-4Q2x$p|` z$=0scy4E@YV4T%EC7FGd`7x1U|6VwFMMPd|C~W@$0Wuw`pd{*}K__)5 zL0!3IK%+XL(_6@`xc=~jQO1CSlAxlk!bxovn z_2j6WP}tcZ`RIOPDda?X;69zV0LI|;Z3M|a16j_&q~!=I@=pMvPuNzOtr_PXl5JA_ z!ac~t$)l4uop(X0()AmG^!O^I&;uygC)+R02YT5`h2us}M9+=!s6M-t1F+BTnrRJB(0GnC>+Xuh)3rm{B&rL^Jm1R z`{^@K?pg>)O6Oxqrer{B+Mn>LIChDt?f1}Q&`F>dRXd-O1U4U1sDIT?ISPLPEGED4 zkb4x=n{@sP5au8?$wXMzf2yqTT8we6%GS1|^EV(9rn1aLB$5ahA_;VIRvcr^O7&HRQXN{?Qhc_P$<9BJucyE= z?E}jrUY=_I391@dxvLM^|H4BejP5;be}Smo1q)A9>H=GpQq+W5_!21REB^c&KSx?- z?rVf|qjRS8_Glhd2B#H)zjxb>(4ng`xLCjJXqWE8f+D>Z0omIh90c zN98@J&Z*mQE~eBt{5t}+a5er7LY#kuW6Y*50_;xwixY@H_T%gaH>2Hz920C~hG}nx z0t5q2wja27udj6?pmCSOdJQLvsDlwjZ2T{PH#F+;eyE=oQML7Dsk(Dsk{$cO7kKcC z`@whh;LrDiZ|%XOiE{o!Joo|S@RDAheZ@`?kblGcwMG_>YmF=k7fmVly7nfQVyR*F$P|@UyAYEP7w4)ZP*rjiaLf7flCE8M8e!@=EPf!)hApZ*ytqz3Bd8M#_dKigj?v z7;9~j86$jo1L^~wJm$cW$5KuLzpNsH+Axe(^5b9_0z1;sUgY1#9}n8gB@rM8gT}$= zC|2mjXjvv=Cm_;inj9p92oo`4Y_#VzS792`n``Ee7)UX9IYGOvs z_;Ig-ChhsK^h7u!5PJy0za$tf8Y7FwHE1DRqCSU{o&_M~>h!t6Lv>*|W|mRA1?ez% z(-Sz1@`m!+u(u%-C{)kq4pUEdSIB&h>rOjDzd%{psLT~Y`I8{5Qdw+Sm>mr^9IB}r z;`Sa0?O|YtE~-iCM@nf|K@Bxk&h3?`aTU?ib*}P8mHQ$J5J&baAg?TrMx>NUnV;uT zM`0t!Up-A)}!3Ig% z-dr;9SJFhsa6A>*t$6iI)$$(9i#r#uCMw2k1u*?j8z6cfVQ&Gk&^lqhRfav5GNz+O z(%zb}&S@y3y%j0!PG*8SlCd7TgVZDG3&2YN$>zWO)HH__ISqK%&qW#A5Xar#cdS>XhfFKKTVtF(o)SLV|V*pT{ zYgI|zZINQ#u@JiMqDBMlFV=wq&{r&kIBD^}+)8ePuGHu)F$HNut*OyQdY2MjhkzFq z(~ncSql9LY`K4872aIo95s@)i0fc3&M`E3rA==}L(eV<+x@<-wSQJKc3hPO!dnrT- ziy7EEvwpn#2yslQy2L-(I{?w$VC2xUF-)I=r%-k(e$y3(o<$`vZrM7ZFQGGXa0KBH zw;<{yE1g{O%Q8Qmp6f+7LrvXmuQ9e)^omR=&92)F$pTvnO*CTE^qdSz)1H9BWGCV$ z|6?}n(29d-h-jhkL~I4)&V)eV7j`CrC}E~&W2Foe;n=nFNf=S}5qmN)+SSG6*tLw_ zIjA8IjBmPzn>li~q98`OJ>p7D3>BcQfGb)?to!N>NlP!P%aAc@#r%Om&s7v8Gp7Pbyl4&2Q|tG(Ee)*3H1v{6T#y(E3(Cix}31Wheoq=eg$0hu{_Q&6duA*f@lnQ9wQ zHcH?XJ{8+wcBo`*16Nnk1ncmlDpZ}NOx=fs)NDYuVEMz85vzqT95ni76>1z^Z_gqt zl-=wOHOj7BBbufIsEOGqC5T}Q;X~T*>PhGtw3C4%V=MLUryeJRzuHplkA8Z{mbGST z+eR2?^W4<7b?JJuwQ6(~9$Ty7v%^^03F~zoc+FNC1~=Oz0zjJ&kO-!|=xD-cFXnMR zD1jSwIrJIgQf_ObS{9PEVM8`SvU9*;rPtJFTy-%#bj843_?tyLScfnq*{#&=FD%=Z zC@B^trws8kh&D+EVa|{{CEn!DLa5WQ-C#=A?9L&|UWljcVPqLfj)`Ltg&Fr5aK`Y` z!`*{Gqa!a-_u$%aS1p~Ky#&n`vWOo~V&?cir>^wfS<-zJ8C7cThXvf> znCW$~YD=e!Q2>1|x^??VWR<7mWglN@AH_%UzcOAK3Y!&Q66#KbUFuuT8+4h-uVfoR zXt^G+t@xIqaGWu7hX?>%l!5c+SP)YpVJ4#?I-+z7dXE4v82bX{|Gx_U@; zI@%KSEFk0jfqmk3~M|&q=qw8Fk{K3)Rb%`jnF?`*Y3iY=HfP(-@)lVoDMY_hM#&0TAa|1@M-?(0b@u zPhx{9JA8X_%D%`)ud`vqSSjIP*Ifnv4cQ|QZD?@NUBx;=>jO&nmlO(i5OYvNev+3r zN&>}jUG#Lc4XlZwAEbOmdED3#wJhaSqtp(n7OTSj@Y@K!3ww@nK^g8MfusSc_8?~m zkQxv2qCjfEm2(OQ>gQI6bd)t#Sk{A_r2s-bv}5Tdt0OiHAnjn88pmkDc?bAt%6p46 zv`)jTN@&!Zzd-sG9RBEYFI|a<6>*j!-HO8jyfT@C$#Wc7Q91P_`VQHo1I4}b0Dm{GiU}9 zc!NqNU_4fJC&zCU>RdIox?=f@Cygx3n0XN-4Gp#n_MYit&$^%KiXBE2 z7OA{4jw_aNZ&s{td(BE66 zZ}2MV)nWqeUQkDrH-Y@brxz$i%E_W$RmvYLht)2?XK1JPa$r{6f*R`Hym%fD9b{vH zrHHuIt#mF}f7i|hLrC;kF}xnoRV}<5HE(EP7AQ-7=|s`OBx-@Sw#fIE?lKUTm-}Ku z%F6AP1AAR+g7|hDiyjycpMu}{F&@>37s%RKAtm#UTc8b^>O-WN$TT>QWY}n=Y3Dc) z#e41mBH|p6IDBO^_aj7X3uxIeI}=E6!whTGxs^aVIs9Z-;Rox58xHGG1s2JD8^R9p zd|D|bMk!GTwvORKBj5#zH6eC_#G370;bGRqy0THm7up>?mxJ zdFR*0Pe;7$2%A5R@Y?=3mD)=k_7PVEaWeMtm-W^N^$|nAf$1RiQ8)=1xu3ORGZxm7 z<`V9yP^i-e4_Mvv8>k@aU^ozi*RRU`7_>GF3I0EssO-5ESSxH*u9aOx2i1WioDnSk zz@TxE5sxyONz#FLEdi~Wbih1@_167;)eRZ0mNomT(6jf#yG zriDVD=nH*?p|-G&gY_;8GtbHMsIsUmNR8i8RE8z9sJ9ShV&fCLl&~%BSC*=poZ27j zHSm{h>i1u2olR=(%hqA_r`^b(h50osyLJRD8||vHv>hK4w974lF1P1B2r0AY(E4cs z{plOgU$qfEWMn%=B5(c0IRi!R9%1CpMChD_pRU1$)d&n{4IyXaM^^xP9XML|J@7cTY94h7tTT4?9UOcjf((W48W;U z*~Udbz2HBE{EO$--RhBndSWHu&LS9dOBpTgQUq0dpgBcQO~{O9&xL$d^6QVMA_D+B zcToijmD@LjB~(-}mW+MSY=Q<0sM$RN>`O~BMM^^5M-((F5`(KA;DDYQ`tX7dhr)w=X^x7 z7vKj~bs>UyQ9w5~3S-KP05nWzI07_3m_vaDbDM$tkK+qCApdM&g_<}?b+OOo*!;(k z1Gb1UcJ^ZC3%fcH8-C6uc*w2APvKJh*q7m_tILO9j;G~ma2QBD`wC#uo1i191*gWj zP0p2o;Gl?io3dJa`w5hB#>0&Z+BoNRSQ6x0<97cPYRl6}q#5a}~pxTJk8@a?_C6fP=94<-8Nf+j}x7wkrG zilc|P9`Nd(SD~t?a|7Zvs>-<$AssL02VGb)MAtQXY{L9{@8B?O)ax=@`*#_Upv#ax!%$zTaa&cOf8?}xk@{$lia6t}i?1->#kkYT zj)CYgGnBs>a+~4&EeJSuWgVJ1J~_8T8iC8p7kUH6%H08|j#t`uB96HZTE{}P{U4!= z6|_URgNnw>Fk`EnyAa_C7tGJ@MwI0>brv#(*ON6KoQi?sUIggZ^=8du8{$=6kt%IKR?i?m=>Whz~$#A4bG}1V1<_)QD}LUo%>bj7Vlx zJC7pZb`T~)vF^7}!Eu}nk9*|<*<-*vj|)+i^BaaKl#JzIit$~n_!>+$G2!$RMG`eV zqhP8=j_oe))!h?F>mG_0?=HfR^IJS723dm=wN~x82hi}WT6uq#J4)Osa@AI9fVSdH<FU9J49ex-RoV5Eb<3!{{`1lUtJ}7$Q zs0;>%S_*CLaOO}**t4&uo0WcV`^=lL4k`I#!*1RoGuD5ivNP|Km|X`%ez(bWe2w3?^GAn> z_!s>AqCF+2D~@@8w+x!FW<_x34Y2C@1mgrGet6XFduO)tc{^Lbd`M<&*dr#h4Bptn zi>aFUmBCLRE=(uRGIb zCme9jaU$}dCtf>7xZm;7p35??_}mH3JVow3I>D(Qn8#(7`ZiRvbn#rShUZ&fZmEg>~ zlrE;}M!TvhD@Vh za!Q-|Cz&3oiLX%id?+G6y4{WM3-{9+cl=Z4NIwC=nf+mEJzYA%`ghlUkg4<4(R0(s zA7sw*1-Ue}9uX*q3LkLhbjNYpBA@NL=eKLBApW^LUFoI8{SW4WJE#;#@6huMePG z6FN^r-wU7_Lbun@who_fJE5aBbWs2uL@3@LBHx<>=)WnMp`qUf&@Tx66pJUI-F7pd zZy%wrYUsiMYLf3G8hU;JttAvMc9Fvq0W?YI`5O9h0A**_Q_#>3+t00o&?7W-N&uy4 z_RP{yH-PR#=mZVDB7h!GXorSA5kPwht<%uI2hh_A{Td5hkhgwwUxpJ1eNRKj2heK> zeO^On1<>CTdas7A3ZPFDx=usa1<)4>U9F*bquaBGKN5PZhV}(e-fh`4UqeT9`m`St zx{HQR3!tA9x{Zd;51?NYI!r^)44?+xYE(m?4WKbXKgTjY^w5&_bE_xx4Go}G13S0_yFn>`m%;z6+l-K`jCb` z89+}U^m+~bIDno;DBkEK-`df>3_l@sm4=Q9pcfIkNJ9?@pjQ&Qmxi7hK(8ZoyoTNw zKyM{6Rzp7zpbr!J6^4?KtZoZmvfmQ=j)qPNpf3>mtcETMpuZ>dZVf#z zfWAfORT}!s06LTdtTQ$Aod9|pq02S2c1vFd4wHHg($Mh%^btaL*3g3j=#zww(a@tWPS$}@8!1F57r8rSbmr`>smv)}i zv%8v}!;xqsbnjC@r@(Rsu*i!Oa{D?=ZvonNzywcw2^t5)KI37(hUgn-_N<5HbYf%d za~}4g{#eYd)4k7o*sBJ_a_yJJz`19CiduL9FlRk}+|NZn!>Eqm;W=UHNo)y)F_s#{ z0id+=A}Q+2<-^b^?Yu-pqUUbpP@k}0=2L^JZ?IoMtaS_hQc-HH{VEZI0snmoym77n z+R@pI6G_Xm2G^&Y*Few}vr=a-wvv5e>?9=`t;Vj5vQpjAPHT)Pb-*~9rWg2KH464+)vsNY1Cd(cf*Q&*gjq_aV=koqk|#!uq(eo%d)FL^H( zYy4G@9vt#4FSY3XZKR-Pc>-RmZ9Uc`yiL1dqPCF`psYGaQKCOFPp159f4ipf z^#fwx^{{sUYuHz7gS?gxviwjY!ZZ<~y}t+@6yZHzgd+z^xkyrO)<31ql<)f~w_?iD z!r4F5!ZZR=3YR^k-VZ#vzs6{7W9)|>_Kp5n(KL4(sIQ0mV=>N5_kQGIuNn~hXAgVQ zfY^^c?7{)DpLp0E`(x=pdO!8B_JG*Gc-Yv0*uQ$%Pf>z0Z3ySTdD!O%#QxpG-Zmii zGY@;-fY{GH?D7Gz|M0N02gLrvuGQna{_2(FXH7Ls8~5qO5^YwS zH7p-P6p%QA?)TAcwDu)`UF}sSW*#}rK*ce;mVV@+n&)k|j9kqaE z2jQm|3l);~#sSk>9(MJBSlqWKX_pR&O?X%?V{9x{gNGdhY9XP>-9Mb1G|&$r}}$kZ>!fI@Z-O$ zdupwPbqCVlk!t;2O5)sS#sFXKfI(dr_f%qcJDhJqh$~Z?5nxn=O+VA_rVr^ehX9mq z!H+!}0DBAk;J&vl5nv>U^R$pFoZdNP=Ey=O2GuH(0Mtykg~!JyDw4CA5m@!wHA9)RwUO-k=yWt_;AKY;Ac!No<{O1hrT2|*1Z@N zQwnm|vEmQq23R?1rF%0m$8p0BZ|CPmk;z$L($#>cc0MsPT-V?o;o+NKweFMTAN7*C zua!PoQ8ULf>upBf5YPZcY8^Tq;H4E=rpd`eFbI!2xF{5RmarIXVdI=dNIi(M??j#U zcsjjP>-3k@DYi2i+BQ<m0#LHS-A3hZ4$AgkV(hI^1-pMkn@?A(@deUgjyVyUZ@#~j4PzJuw z(T-Z)nPtJvP;%GLU62FzaW?{g8x{FYV1C_{Xj@28xU{?&!-*X&#URGsjx?8*(~Ka^ z7D7Wz;qr1ct~NBYTMC*Oy9X?g??i&(;A1up5~mASlqVWR?ADTq7#oKo(l$;BvRzF; zP|~iZqFu2}Agr&yQ^$}+T zBDm7R84GSWzvy4)_CfhcIQ^oBeiyG!4I8cf59{M=@ZN>>F$raZ2MO8OWH(c5-H+KI z-UfFE_v^;YRrj#K+aZcV!YU%knx03H*4z0mTv@KEc512z)a|{*6s{^qcMv^J(8Smi zp*(0Gdu3*JJis=N}idhYk6Ocr3He z7gNFeFJaXHyWEv}n1WpBP_d#$yTlSt7$+6(M-Y^C9Ue*=6<%*Qvy((FVhULQDMfEi z^khL3V^7s(ol5W&fbD4-FpYqz1WX5@@UwC*oy1NPF2wMH!89+nqv^rlvZ3z9hB}>e z!B{D1sGE73@y39r`vs~u#hc61RnByVr7hoxnGb1vb3q!d?)%z`Y}QLls~f9*6d| zBLOoAKyykLt}Rz_n%G@{ZSN( z7;YRev%8Dr#Mn?0i@P(yrRASNK(YMSm2<)FuU~#*Hd+21z4C7tT%RMwFDtpT2c%K| zspOCHJY-m%gL3c5a_@rNc_!##WR0qQ1c9>m1Q!%!S4xIKd5o7Yq;1f%%~vOD(mO<} z*7AIhUI7XRi%{lvH1EAY=-`HK9`2ciz`^Z^STvJc7h(=B5s%@x4=-ViViB5eqdbtBJexfJxH-}`z`Hg=OBHVBkSMM`r||t$I9+W+3~Wc9w0mpVS@It z7xSXoh{4E7W=Jy|(r8l;L%}nB-#Vdqv^h7IY4!shjxw9hJUsE_*#0^cEQUG<@Uh&j z{7QRV*84&5w@`|eomnQ|Z%XCE$(H{pAN!z2@bN_K`3_NE%D~Zeh$ReSp_~ILQh=E+ zm^r{SI0tFjEmK~hbq>a(Ghf4;LlAOqjbq#6eu^wuR>GV^@l&{=yy9_1vYA}~M0+nW zymT~OxUrmOOVTV98e;e%g0#Iqe)uvE-rA9S1=2-9PZvQC072Uc7hbHK!;ut1fYv#J zQHoT0#QQ6pRMwha>M;qqHwzE0E`;@zAC8B1g0Z-QE#lm&KEhVGsa#g9b(-15A}cZW zf#4A1i~2kSzBiCK1pX8do%8T~kbeF>S%mVh63jE&ZO8+rj=n>VzwxD?hIo3AKJXWk zvwm^{oAO}pqR(?{+U}^n;6Yxgz!~d!+6v)SH=dYla+W|W@94Jp5)4@NI~{n5PtKmn z$Xq6RUCouao+89kgC^25NX_xzEtK*gt$#-a+9PE3USmnhYmo7DYp8L};qWi*$-_;bo8k#!%-@g23+1_{lDXGFRgLRMSC3-*$cwEDq=ElkSBe2)mEqSFTgbE+cJi zxI0pd`)v3gCKNKn?HmVQnsfr9npD{0ONo#w>>Pu1nEsZLo*t08im7LiJAg+@I zSb1#*TS5wM17!*6vQ%jLP`z@NgRaB7!Ikz*n}iPlt>o^k0KV`6bN-uq?#BB{)k#B-hd$T zhU9y&aA%ya$Xk%{>`Kt#l?xd*&Bd_6!3Q>QG8%5Hb%gh3WJK-*N^fI!S6P)dtDl2L zzAA%TdZW%RoQbSrc6fyd0j^)Hieg|@ka#epa^42rn9Iu&L<%34>pM$*pMZqzy~J>e zu65)cW#%0u?I=qd%OE$?#6$L56o5xt>ngNs;%k%@A!*%*8v3r8UhvG*$h`K&hJ$0n z`br3ij4|xTX-@bogGuZhViC^u!g&mNcjr6EA0FQ_TpYb$mfsz4BYPtG@6D(UoSEN8 zpeGTEb66nwF{9*ZWKU+4Y=gF53!AI%aI3*jzx18ogXr4I{9Y28`I!<%^0OovCI=W> z&qulQ6bm0K@i2bEp%LBy@rX+vlln@RRD{2$TyCXVKccF({-t)nU!YaTHZB_caWHWDv)2Y4~h8- zWwB3!9@Tv6?QCIhOTiTQA6kADSIA%cw-8@G2DYV+aB10nIo;cJ)+v#E`r6Z zjKE%Ko=FFfX+6=Vufej4f;+OZIjJWj{}0wkDF08^2wq=&j&|`m9t>3XDpt%-!9C2a z^^$wBBWh8FQfRkUHt+Gli4yYpfTByClP(2poy_De@;7>q7cRCAb=N3Rka*8`5>|tRsx=Qm=t+t(tfD*A+s1w@JaStt?JBO1UcHQZ565 zCo3AGk!}nO=nR(e8t}A_LK!>2nCo;{JjEL?C|{}Qv2?8|qQ@i7OYj|Vj4jZ9H$=y! z$idD0TCQ5vY-_pDCme{PVnPvKa!6NJ|1qaj+^G_guv=`b53l&N^4v@aj()hMA{AsY{ zBA$J>lowwIfUxkEFtwz+6XXcHCs3NtA$}HB6-Kggkir+?K)^_rhPr6z-UG!eA*i;+yGeq2(rlD$mo%b7Ki1F`54YG zLC`r1AC;G-dj=5H_Q|_n0zWUkcVS$tZn=AqV{gH z!~Goz^5upvbH z%++iWw}8C8ml%5)YOH{hd!;mMiZsO7$AHGU6=`BUeGn1vpdlVtE#Ad55$EraffJec zAuuK~5eG*+0BCXtPb8o@;^0&V07Kl}36RTYsw2)n0Q6tG+aUzb2D#&34FA$xHX4cP zacfgedWgF>66zGIvD8y`Bba2*i9fHmsxxw|l4zB7km)^D6?7-#(>4z!*FQ&`e~&k3{ibPMCZhN|8D2Lb$8oe%hq0DjNT2Sg?Tz^~r< z0E+;A8_x#}A%I`d^8xJyTvq~sSj*&Vd_Kiw0?sO-m_Y!)?B`SLO#r|D=K~HVU@?At zzzPBulu+aeIHiQ*TmtY8jhCdCb))-+kHYy}usu8kGKfRKW;Is+F{QOI zj34tFd{i2j6YK(e^%k8W3;U5_2+^Thyc1XM^{GQfuyU?K#?0X$7~buJH_<^2rvd6{IrX_kYGO1szCOv%L&8=<+@DYw z9a-7)K*@<4&c$QqlVczYY@9|mRHwUfPJl4EzgCEhRLV}nG%?Tmsh>9SF(C6ds(r19h z)eO%f@F$b}SR!`T^hWM>{xUw6+MffFoI-J)N6hYq$ffu1R$9>-d^oXm0esA_^#?Rp z8rc_+B+m*lvg;Y8aGo{LJTR(}p6CRIGy{nM(dz>w0>FtrKq6ZAP?p%Dk^LQc=Rtzm z3<-RL3l_ZN?!RHE`i?tfb3AD(+qHIX(vG=-kL*L*7Lmu=F1bh#Ss$z-k5o9;C%hB#4=4cF>lL>ia2f4~U=Hp0&JIO(SV`TQ$B*dHTWQDC6tjJ>^ z4@2h{8dzkso(tG|xSF7gtkPQ#luY)&n^YmjL^)=nnL3@QW7Li)D(aPv_7#@35*eEe zGWwDeL8DheUq)7iRbb=1f>_MG3*y_aBD&hf{sd-poZU+o@#1=^cg)#i_U}Q!u~E8! zcVys)E~eCfAaDhM_8$@JB~$S$hQeB${P~s)qdf^&*x`>Q$i9vQdF=E9q~oVBo_&Mh zQyE=#8h-l*Z;rmlSarI@hcj-!Ns==FKvv?zMj%96qVHrLA-SqSHqcT35m2AyoKiu-e?{YA+X+6sV)H@l#qNVr1LgtoIl|QY>I$)0Elxtc(Ivt@ArK!3LMOm6lHPqf5(rm4M14n{AZ;QmNox*DTIa0|AUYmD;q^)wHi4x&Hhv1 ztV&j)vj2tW!WZ~CGFj<-iICsq(RZz3&jjTS+g|}%Y5yC)(pBT*Y3Cuib37^mACAWk z5rP61hv}AkC2Z!+Ir1nW^5NY>7hqm@EO;8(d*N2{XMx1Xehs$yvn9F>I>G!;B#IG* znLkIOTQK@lMu)8VC5#atPJS1yr!9&pRrVXC<{W48lyXR;dtX0DllxgeNVA)sT9zt* z5p!u=gGq{kTplgMy8BPVHDB4w&;*&`wCSZu-38kR$o$13_O~q8C5#3O1oi1cSBGwwR;c+noiAO@6O}~y{SI~P1oA!NN=b!a&H4^NH&_2 z8Xl4_bK2j7CmJdiS1FJCJjgr(57M|J@=zhqZF6BAOR~ZlQiyu*ghcE%Y#2JurQk!L zM|x>@L0VZXw!(#axN}*EvBaepS@7R z#_VMhHf1kih+``Ja?U7xCa1>8(cT>c1#v?B!FvR;DyI^^!MAh9yei)d#sUeKN#t?A~{$?Pho)x|V&;ya2$fN(Ym+0SRsMa*7- zpQ34M!_|qz*N91$l)})~yC;*1j5k3_Sn9`Lry%HHi4VitO`Ko=_ZYwW*xXp z=Q$X7i3{ZpkLh4yL`x=BZwzJ9Mar%~^JuMFa|`;7_M{>0A&a3gU66j#h0eld0oIsB z3qjcAtAgzM%u^_4987ubBhbyhzBKw|zZ6*vgMr4nA~lnRH^ftmZm3y+@HEtu3n}r0 zF*fZhi$N{!fIL>3a-X@B{BUNt{PQ#&a^y?Qalf4~#}4Z`Hf+te#WyiOQ#@Fuzud#d zGwxQHr3a%wq*>zztTyDw`NTMw2GDHu&ZA(|g%jfX=^{LFFPdk*C;QFdT;%K9y>1wl zJM*x)wBvHben5CWLltU~vun;EsXGaQ-0hmOtD#W`@65z}32SwHpP&RhLcWKe`+TG< z&m_G|f5?-7c-iu_nQ044o;mZPU?y&c9$LOSHe6SMJ8f6U#eT!RU^m1*NBIMvCn>H( z3a)-uTh(2DqDx3xo~WtJQEu2B%Q@qZRQPDP6M;p7fB>nUSL(ufBaqp@*lJ@{|%!O9Hz2uxPP7r8LT3W zCn2sH+<-w27H_7mqG>`F{DbRI=oI<=KhE_k2pbWWxs&Qe$hH1y2utB_<9RIL_afw< zPd^y`c@&<>2P_5NgoGKuk+b}9?LPt-x(Ethz(lbHE{?<{Q9vl_xz~|IVBm?PXRJ(UX{N1u6;@;Wtoz(Cn{55k^!9n0#=3=9w z<1_XNyr1VzR)TMXTd^-jPI3G|IST>F4u!Ilyi(8aG967gVKxZ&!OuX1edaQz4LT;Q zWo-t>js;u)ljuf{{*2p3feApT@Jd))e%*A2F<1Q}1wJ{&`i5d@J6oWw|as9QRdl;@kv1$>opb=wv{BrCsD&qYg67S$kw*Y8=X<$k;GcJ6_uT^?a-;s zV+uOuZX=Di4$fZOif`LX-aB0Lt5?7AZNgAfQBb%}D|;V?emt+xg>fI2NV^fh2ur&%OoWVd3X3`SbCej= ztNtFIw)rsc1J~^u2U)RR;cYDA3r+1%Lxa(~?^<%NjQCScn2f_CZIG{;ax(|R%<~shkOj8twmgj#X z^WO|QZ|^0BuUn_3H%nCaoC#yVt(xr85;b_c)42x*nW**Tco-P~_6^*MRqNohDlr-8 zA-*}|mmyV`_q7GfeTBpxhGSU7J2$3!OH&DkRHfrGN`g))Hhzh|rL!8hbXI$pcxZ{7 z47C)s;ak!i%Z%F=js{)0R@;Z*jdvi58uRrMEetDH$xTI-5L2R(x*iOda6z0_iKQ${ ztHkR>{B2}cRAXt?)E8BQS%_7?N$q$TmYXMvf0OCu;iRE1r;9%iLO#q&z-m(} z^f1CoP7L^Eh3KNgaaxCU&|%#GI>dKf&78Es`C@nx`%s41bD`NK63IkEPa}p?*+(Es zVz4zBUzPy^7quj;hPC-Ih%0Mwj-iZ$oNJNR!K}<&fn`7gH^C%%=pbdK#@)s$Dy3a9 zm=w4Xh7`C)psUsEW}l>?X{J_Vwwsz5ji2(wDJI4ZFigzL2mHjpElJ#z-GYfRB0%D# zvj=>LHMr0GHLHCqV#auwJw42$0cLAr`U#xCy#!W^F5>klB8D~XiJ`t&ZW{Gy4K0pi znv3H+pGNEG2Bb|4vxaq9xUygxx>{!&Wvd3>a6l*ED~yfLy&pM9OZR&X zYZ!}2r7q%{J< zEk_gZ-K$OO?yQ&sg}9jpx^dB(BsG;o%>K+u64%Ra1HS zjg@0{KNnV?fwlmH&Tb1WbFXqcu^99Yk&fh=ColkVdH{*1heVulh*Z~NkAyVQ?0Eb- z8T|O;|2NCV{|b9)Wjt!qE3y+n&Vh}YpNJT~sV+?OlgLG}tIJJBw7t8TuZ?19V+x+K zQwd5$@!k0zMW9U)ezWUA#>EMGrrvWqWc3o9a6CYlV>1qF;r5C;rVkT^*=NZhlJ(viBlW1~zc`{vTqZRPoxyZP8q3(^z3azN&k}=cGY^*yn zeJ?RK3?=RCglF6}ehSiOp92BM*s#i-0nSgPwde+no4xtu%mCc!g48&>EuWvocS^0Q zKJ$6<=6Fo@{{8x_ccMm}zE|Nuk^KGscAaChD9o>DFUKH-@ zfp*G8qj4N5_&_QrQG>fQq|TVJYz1H_B{N75{vF^LP_Z^kD6QJ;Ec8Pd(D*2}6Im5C za}wo(X!at~_|>mD@k_Y1mDx9d=J#-!H!f4FMUP)YE6}?0a@rgaRa@0CGlgo!o>#f( z^;UgX6~-I1Y<_CXZ*P*12HW#7Zb>9mrI%46<@|zUaeOeDGhq+f;B+!0_2!+(p#vX2 z$H89gP}N4jT&#E(~PmpmnF`R;VFSQItwY`8O?yd*2uaXb; ziRnt+jY4@6NfgiBm*}J{auv~)M5l;e1N6+=nEM-&rHXyFaM_?olU750V{Mgv2+G0f z1G@e$-0fk$`7qHNS8dmWx z{x;L3ZKl=Fyw7}^y4w}n&F)6~>NB4uV0QxMk!_n#s%?~fVLJyAHM|@}^Yxa|&m?+; zht?I1Zlft%Abey1=h_orY{K3I5u?iaF)Oc>InY=`tyW&ZWDRFWjp0Fab|q5{QC1@o zehOVNHpKbJi#0hPGv--Xqt)zGgUtZu4b$Gxl4xf|zrqS`FR$PYc;B>>>yWg5Z0X7x z7rcgiXB8m%djoEu3$Z9XAJ)GV_IyaLd^k2B_--V>KUD@lA8?#=kabAX{|e8)hQ;L3 z7~oO_J_Ik#J^*+x98JA4@{?fWSg;BeMwB;$TYY!d-b+l$lyNd1%^ZJa64V_J+hE35 zon=)9Ynr_O9jYQp*x3WMZ=c9UbP|5>)`s+Yl?P#>B-5m5YIMZi0gsqYF0$Yav6AO6 zlOqeVuBjS#ItyAK)(3>9fU(NH015FS0Lz=3-+(5YpUNx?Bk^0(qHkkSW)<$KT28dMe=5>TB)&5JNBrfit%xK96x!sqI2@ocD`T9!J@0 zpp=?{CX6^O-%&2cokcN-DG_53#2BQ-s59#8v>0_IVtDa>Vhk#ZF$iJ|8c2*1<4>Tk z8;5;9{28fc_cQ3-a7 zd1bUtX>ucnlq8&v2eNNCv@~H`FGOi_p8+t)qH@82l$=*Ix$A)pDC-t#%0Dplg;7Rx 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 z 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 literal 584192 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 signer preserves as much of the input APK as possible. For example, it preserves the + * order of APK entries and preserves their contents, including compressed form and alignment of + * data. + * + *

Use {@link Builder} to obtain instances of this signer. + * + * @see Application Signing + */ +public class ApkSigner { + + /** + * Extensible data block/field header ID used for storing information about alignment of + * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section + * 4.5 Extensible data fields. + */ + private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935; + + /** + * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed + * entries. + */ + private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6; + + private static final short ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096; + + /** + * Name of the Android manifest ZIP entry in APKs. + */ + private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; + + private final List mSignerConfigs; + private final Integer mMinSdkVersion; + private final boolean mV1SigningEnabled; + private final boolean mV2SigningEnabled; + private final boolean mV3SigningEnabled; + private final boolean mDebuggableApkPermitted; + private final boolean mOtherSignersSignaturesPreserved; + private final String mCreatedBy; + + private final ApkSignerEngine mSignerEngine; + + private final File mInputApkFile; + private final DataSource mInputApkDataSource; + + private final File mOutputApkFile; + private final DataSink mOutputApkDataSink; + private final DataSource mOutputApkDataSource; + + private final SigningCertificateLineage mSigningCertificateLineage; + + private ApkSigner( + List signerConfigs, + Integer minSdkVersion, + boolean v1SigningEnabled, + boolean v2SigningEnabled, + boolean v3SigningEnabled, + boolean debuggableApkPermitted, + boolean otherSignersSignaturesPreserved, + String createdBy, + ApkSignerEngine signerEngine, + File inputApkFile, + DataSource inputApkDataSource, + File outputApkFile, + DataSink outputApkDataSink, + DataSource outputApkDataSource, + SigningCertificateLineage signingCertificateLineage) { + + mSignerConfigs = signerConfigs; + mMinSdkVersion = minSdkVersion; + mV1SigningEnabled = v1SigningEnabled; + mV2SigningEnabled = v2SigningEnabled; + mV3SigningEnabled = v3SigningEnabled; + mDebuggableApkPermitted = debuggableApkPermitted; + mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved; + mCreatedBy = createdBy; + + mSignerEngine = signerEngine; + + mInputApkFile = inputApkFile; + mInputApkDataSource = inputApkDataSource; + + mOutputApkFile = outputApkFile; + mOutputApkDataSink = outputApkDataSink; + mOutputApkDataSource = outputApkDataSource; + + mSigningCertificateLineage = signingCertificateLineage; + } + + /** + * Signs the input APK and outputs the resulting signed APK. The input APK is not modified. + * + * @throws IOException if an I/O error is encountered while reading or writing the APKs + * @throws ApkFormatException if the input APK is malformed + * @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified 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 or verifying a signature + * @throws IllegalStateException if this signer's configuration is missing required information + * or if the signing engine is in an invalid state. + */ + public void sign() + throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException, + SignatureException, IllegalStateException { + Closeable in = null; + DataSource inputApk; + try { + if (mInputApkDataSource != null) { + inputApk = mInputApkDataSource; + } else if (mInputApkFile != null) { + RandomAccessFile inputFile = new RandomAccessFile(mInputApkFile, "r"); + in = inputFile; + inputApk = DataSources.asDataSource(inputFile); + } else { + throw new IllegalStateException("Input APK not specified"); + } + + Closeable out = null; + try { + DataSink outputApkOut; + DataSource outputApkIn; + if (mOutputApkDataSink != null) { + outputApkOut = mOutputApkDataSink; + outputApkIn = mOutputApkDataSource; + } else if (mOutputApkFile != null) { + RandomAccessFile outputFile = new RandomAccessFile(mOutputApkFile, "rw"); + out = outputFile; + outputFile.setLength(0); + outputApkOut = DataSinks.asDataSink(outputFile); + outputApkIn = DataSources.asDataSource(outputFile); + } else { + throw new IllegalStateException("Output APK not specified"); + } + + sign(inputApk, outputApkOut, outputApkIn); + } finally { + if (out != null) { + out.close(); + } + } + } finally { + if (in != null) { + in.close(); + } + } + } + + private void sign( + DataSource inputApk, + DataSink outputApkOut, + DataSource outputApkIn) + throws IOException, ApkFormatException, NoSuchAlgorithmException, + InvalidKeyException, SignatureException { + // Step 1. Find input APK's main ZIP sections + ApkUtils.ZipSections inputZipSections; + try { + inputZipSections = ApkUtils.findZipSections(inputApk); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed APK: not a ZIP archive", e); + } + long inputApkSigningBlockOffset = -1; + DataSource inputApkSigningBlock = null; + try { + ApkUtils.ApkSigningBlock apkSigningBlockInfo = + ApkUtils.findApkSigningBlock(inputApk, inputZipSections); + inputApkSigningBlockOffset = apkSigningBlockInfo.getStartOffset(); + inputApkSigningBlock = apkSigningBlockInfo.getContents(); + } catch (ApkSigningBlockNotFoundException e) { + // Input APK does not contain an APK Signing Block. That's OK. APKs are not required to + // contain this block. It's only needed if the APK is signed using APK Signature Scheme + // v2 and/or v3. + } + DataSource inputApkLfhSection = + inputApk.slice( + 0, + (inputApkSigningBlockOffset != -1) + ? inputApkSigningBlockOffset + : inputZipSections.getZipCentralDirectoryOffset()); + + // Step 2. Parse the input APK's ZIP Central Directory + ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections); + List inputCdRecords = + parseZipCentralDirectory(inputCd, inputZipSections); + + List pinPatterns = extractPinPatterns(inputCdRecords, inputApkLfhSection); + List pinByteRanges = pinPatterns == null ? null : new ArrayList<>(); + + // Step 3. Obtain a signer engine instance + ApkSignerEngine signerEngine; + if (mSignerEngine != null) { + // Use the provided signer engine + signerEngine = mSignerEngine; + } else { + // Construct a signer engine from the provided parameters + int minSdkVersion; + if (mMinSdkVersion != null) { + // No need to extract minSdkVersion from the APK's AndroidManifest.xml + minSdkVersion = mMinSdkVersion; + } else { + // Need to extract minSdkVersion from the APK's AndroidManifest.xml + minSdkVersion = getMinSdkVersionFromApk(inputCdRecords, inputApkLfhSection); + } + List engineSignerConfigs = + new ArrayList<>(mSignerConfigs.size()); + for (SignerConfig signerConfig : mSignerConfigs) { + engineSignerConfigs.add( + new DefaultApkSignerEngine.SignerConfig.Builder( + signerConfig.getName(), + signerConfig.getPrivateKey(), + signerConfig.getCertificates()) + .build()); + } + DefaultApkSignerEngine.Builder signerEngineBuilder = + new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion) + .setV1SigningEnabled(mV1SigningEnabled) + .setV2SigningEnabled(mV2SigningEnabled) + .setV3SigningEnabled(mV3SigningEnabled) + .setDebuggableApkPermitted(mDebuggableApkPermitted) + .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved) + .setSigningCertificateLineage(mSigningCertificateLineage); + if (mCreatedBy != null) { + signerEngineBuilder.setCreatedBy(mCreatedBy); + } + signerEngine = signerEngineBuilder.build(); + } + + // Step 4. Provide the signer engine with the input APK's APK Signing Block (if any) + if (inputApkSigningBlock != null) { + signerEngine.inputApkSigningBlock(inputApkSigningBlock); + } + + // Step 5. Iterate over input APK's entries and output the Local File Header + data of those + // entries which need to be output. Entries are iterated in the order in which their Local + // File Header records are stored in the file. This is to achieve better data locality in + // case Central Directory entries are in the wrong order. + List inputCdRecordsSortedByLfhOffset = + new ArrayList<>(inputCdRecords); + Collections.sort( + inputCdRecordsSortedByLfhOffset, + CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); + int lastModifiedDateForNewEntries = -1; + int lastModifiedTimeForNewEntries = -1; + long inputOffset = 0; + long outputOffset = 0; + Map outputCdRecordsByName = + new HashMap<>(inputCdRecords.size()); + for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) { + String entryName = inputCdRecord.getName(); + if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) { + continue; // We'll re-add below if needed. + } + ApkSignerEngine.InputJarEntryInstructions entryInstructions = + signerEngine.inputJarEntry(entryName); + boolean shouldOutput; + switch (entryInstructions.getOutputPolicy()) { + case OUTPUT: + shouldOutput = true; + break; + case OUTPUT_BY_ENGINE: + case SKIP: + shouldOutput = false; + break; + default: + throw new RuntimeException( + "Unknown output policy: " + entryInstructions.getOutputPolicy()); + } + + long inputLocalFileHeaderStartOffset = inputCdRecord.getLocalFileHeaderOffset(); + if (inputLocalFileHeaderStartOffset > inputOffset) { + // Unprocessed data in input starting at inputOffset and ending and the start of + // this record's LFH. We output this data verbatim because this signer is supposed + // to preserve as much of input as possible. + long chunkSize = inputLocalFileHeaderStartOffset - inputOffset; + inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut); + outputOffset += chunkSize; + inputOffset = inputLocalFileHeaderStartOffset; + } + LocalFileRecord inputLocalFileRecord; + try { + inputLocalFileRecord = + LocalFileRecord.getRecord( + inputApkLfhSection, inputCdRecord, inputApkLfhSection.size()); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed ZIP entry: " + inputCdRecord.getName(), e); + } + inputOffset += inputLocalFileRecord.getSize(); + + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + entryInstructions.getInspectJarEntryRequest(); + if (inspectEntryRequest != null) { + fulfillInspectInputJarEntryRequest( + inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); + } + + if (shouldOutput) { + // Find the max value of last modified, to be used for new entries added by the + // signer. + int lastModifiedDate = inputCdRecord.getLastModificationDate(); + int lastModifiedTime = inputCdRecord.getLastModificationTime(); + if ((lastModifiedDateForNewEntries == -1) + || (lastModifiedDate > lastModifiedDateForNewEntries) + || ((lastModifiedDate == lastModifiedDateForNewEntries) + && (lastModifiedTime > lastModifiedTimeForNewEntries))) { + lastModifiedDateForNewEntries = lastModifiedDate; + lastModifiedTimeForNewEntries = lastModifiedTime; + } + + inspectEntryRequest = signerEngine.outputJarEntry(entryName); + if (inspectEntryRequest != null) { + fulfillInspectInputJarEntryRequest( + inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); + } + + // Output entry's Local File Header + data + long outputLocalFileHeaderOffset = outputOffset; + long outputLocalFileRecordSize = + outputInputJarEntryLfhRecordPreservingDataAlignment( + inputApkLfhSection, + inputLocalFileRecord, + outputApkOut, + outputLocalFileHeaderOffset); + outputOffset += outputLocalFileRecordSize; + + if (pinPatterns != null) { + boolean pinThisFile = false; + for (Pattern pinPattern : pinPatterns) { + if (pinPattern.matcher(inputCdRecord.getName()).matches()) { + pinThisFile = true; + break; + } + } + + if (pinThisFile) { + pinByteRanges.add( + new Hints.ByteRange( + outputLocalFileHeaderOffset, + outputOffset)); + } + } + + // Enqueue entry's Central Directory record for output + CentralDirectoryRecord outputCdRecord; + if (outputLocalFileHeaderOffset == inputLocalFileRecord.getStartOffsetInArchive()) { + outputCdRecord = inputCdRecord; + } else { + outputCdRecord = + inputCdRecord.createWithModifiedLocalFileHeaderOffset( + outputLocalFileHeaderOffset); + } + outputCdRecordsByName.put(entryName, outputCdRecord); + } + } + long inputLfhSectionSize = inputApkLfhSection.size(); + if (inputOffset < inputLfhSectionSize) { + // Unprocessed data in input starting at inputOffset and ending and the end of the input + // APK's LFH section. We output this data verbatim because this signer is supposed + // to preserve as much of input as possible. + long chunkSize = inputLfhSectionSize - inputOffset; + inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut); + outputOffset += chunkSize; + inputOffset = inputLfhSectionSize; + } + + // Step 6. Sort output APK's Central Directory records in the order in which they should + // appear in the output + List outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10); + for (CentralDirectoryRecord inputCdRecord : inputCdRecords) { + String entryName = inputCdRecord.getName(); + CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName); + if (outputCdRecord != null) { + outputCdRecords.add(outputCdRecord); + } + } + + // Step 7. Generate and output JAR signatures, if necessary. This may output more Local File + // Header + data entries and add to the list of output Central Directory records. + ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest = + signerEngine.outputJarEntries(); + if (outputJarSignatureRequest != null) { + if (lastModifiedDateForNewEntries == -1) { + lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS) + lastModifiedTimeForNewEntries = 0; + } + for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : + outputJarSignatureRequest.getAdditionalJarEntries()) { + String entryName = entry.getName(); + byte[] uncompressedData = entry.getData(); + ZipUtils.DeflateResult deflateResult = + ZipUtils.deflate(ByteBuffer.wrap(uncompressedData)); + byte[] compressedData = deflateResult.output; + long uncompressedDataCrc32 = deflateResult.inputCrc32; + + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + signerEngine.outputJarEntry(entryName); + if (inspectEntryRequest != null) { + inspectEntryRequest.getDataSink().consume( + uncompressedData, 0, uncompressedData.length); + inspectEntryRequest.done(); + } + + long localFileHeaderOffset = outputOffset; + outputOffset += + LocalFileRecord.outputRecordWithDeflateCompressedData( + entryName, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + compressedData, + uncompressedDataCrc32, + uncompressedData.length, + outputApkOut); + + + outputCdRecords.add( + CentralDirectoryRecord.createWithDeflateCompressedData( + entryName, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + uncompressedDataCrc32, + compressedData.length, + uncompressedData.length, + localFileHeaderOffset)); + } + outputJarSignatureRequest.done(); + } + + if (pinByteRanges != null) { + pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); // central dir + String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME; + byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges); + ZipUtils.DeflateResult deflateResult = + ZipUtils.deflate(ByteBuffer.wrap(uncompressedData)); + byte[] compressedData = deflateResult.output; + long uncompressedDataCrc32 = deflateResult.inputCrc32; + long localFileHeaderOffset = outputOffset; + outputOffset += + LocalFileRecord.outputRecordWithDeflateCompressedData( + entryName, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + compressedData, + uncompressedDataCrc32, + uncompressedData.length, + outputApkOut); + outputCdRecords.add( + CentralDirectoryRecord.createWithDeflateCompressedData( + entryName, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + uncompressedDataCrc32, + compressedData.length, + uncompressedData.length, + localFileHeaderOffset)); + } + + // Step 8. Construct output ZIP Central Directory in an in-memory buffer + long outputCentralDirSizeBytes = 0; + for (CentralDirectoryRecord record : outputCdRecords) { + outputCentralDirSizeBytes += record.getSize(); + } + if (outputCentralDirSizeBytes > Integer.MAX_VALUE) { + throw new IOException( + "Output ZIP Central Directory too large: " + outputCentralDirSizeBytes + + " bytes"); + } + ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes); + for (CentralDirectoryRecord record : outputCdRecords) { + record.copyTo(outputCentralDir); + } + outputCentralDir.flip(); + DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir); + long outputCentralDirStartOffset = outputOffset; + int outputCentralDirRecordCount = outputCdRecords.size(); + + // Step 9. Construct output ZIP End of Central Directory record in an in-memory buffer + ByteBuffer outputEocd = + EocdRecord.createWithModifiedCentralDirectoryInfo( + inputZipSections.getZipEndOfCentralDirectory(), + outputCentralDirRecordCount, + outputCentralDirDataSource.size(), + outputCentralDirStartOffset); + + // Step 10. Generate and output APK Signature Scheme v2 and/or v3 signatures, if necessary. + // This may insert an APK Signing Block just before the output's ZIP Central Directory + ApkSignerEngine.OutputApkSigningBlockRequest2 outputApkSigningBlockRequest = + signerEngine.outputZipSections2( + outputApkIn, + outputCentralDirDataSource, + DataSources.asDataSource(outputEocd)); + + if (outputApkSigningBlockRequest != null) { + int padding = outputApkSigningBlockRequest.getPaddingSizeBeforeApkSigningBlock(); + outputApkOut.consume(ByteBuffer.allocate(padding)); + byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock(); + outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length); + ZipUtils.setZipEocdCentralDirectoryOffset(outputEocd, + outputCentralDirStartOffset + padding + outputApkSigningBlock.length); + outputApkSigningBlockRequest.done(); + } + + // Step 11. Output ZIP Central Directory and ZIP End of Central Directory + outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut); + outputApkOut.consume(outputEocd); + signerEngine.outputDone(); + } + + private static void fulfillInspectInputJarEntryRequest( + DataSource lfhSection, + LocalFileRecord localFileRecord, + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest) + throws IOException, ApkFormatException { + try { + localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink()); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed ZIP entry: " + localFileRecord.getName(), e); + } + inspectEntryRequest.done(); + } + + private static long outputInputJarEntryLfhRecordPreservingDataAlignment( + DataSource inputLfhSection, + LocalFileRecord inputRecord, + DataSink outputLfhSection, + long outputOffset) throws IOException { + long inputOffset = inputRecord.getStartOffsetInArchive(); + if (inputOffset == outputOffset) { + // This record's data will be aligned same as in the input APK. + return inputRecord.outputRecord(inputLfhSection, outputLfhSection); + } + int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord); + if ((dataAlignmentMultiple <= 1) + || ((inputOffset % dataAlignmentMultiple) + == (outputOffset % dataAlignmentMultiple))) { + // This record's data will be aligned same as in the input APK. + return inputRecord.outputRecord(inputLfhSection, outputLfhSection); + } + + long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord(); + if ((inputDataStartOffset % dataAlignmentMultiple) != 0) { + // This record's data is not aligned in the input APK. No need to align it in the + // output. + return inputRecord.outputRecord(inputLfhSection, outputLfhSection); + } + + // This record's data needs to be re-aligned in the output. This is achieved using the + // record's extra field. + ByteBuffer aligningExtra = + createExtraFieldToAlignData( + inputRecord.getExtra(), + outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(), + dataAlignmentMultiple); + return inputRecord.outputRecordWithModifiedExtra( + inputLfhSection, aligningExtra, outputLfhSection); + } + + private static int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) { + if (entry.isDataCompressed()) { + // Compressed entries don't need to be aligned + return 1; + } + + // Attempt to obtain the alignment multiple from the entry's extra field. + ByteBuffer extra = entry.getExtra(); + if (extra.hasRemaining()) { + extra.order(ByteOrder.LITTLE_ENDIAN); + // FORMAT: sequence of fields. Each field consists of: + // * uint16 ID + // * uint16 size + // * 'size' bytes: payload + while (extra.remaining() >= 4) { + short headerId = extra.getShort(); + int dataSize = ZipUtils.getUnsignedInt16(extra); + if (dataSize > extra.remaining()) { + // Malformed field -- insufficient input remaining + break; + } + if (headerId != ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) { + // Skip this field + extra.position(extra.position() + dataSize); + continue; + } + // This is APK alignment field. + // FORMAT: + // * uint16 alignment multiple (in bytes) + // * remaining bytes -- padding to achieve alignment of data which starts after + // the extra field + if (dataSize < 2) { + // Malformed + break; + } + return ZipUtils.getUnsignedInt16(extra); + } + } + + // Fall back to filename-based defaults + return (entry.getName().endsWith(".so")) ? ANDROID_COMMON_PAGE_ALIGNMENT_BYTES : 4; + } + + private static ByteBuffer createExtraFieldToAlignData( + ByteBuffer original, + long extraStartOffset, + int dataAlignmentMultiple) { + if (dataAlignmentMultiple <= 1) { + return original; + } + + // In the worst case scenario, we'll increase the output size by 6 + dataAlignment - 1. + ByteBuffer result = ByteBuffer.allocate(original.remaining() + 5 + dataAlignmentMultiple); + result.order(ByteOrder.LITTLE_ENDIAN); + + // Step 1. Output all extra fields other than the one which is to do with alignment + // FORMAT: sequence of fields. Each field consists of: + // * uint16 ID + // * uint16 size + // * 'size' bytes: payload + while (original.remaining() >= 4) { + short headerId = original.getShort(); + int dataSize = ZipUtils.getUnsignedInt16(original); + if (dataSize > original.remaining()) { + // Malformed field -- insufficient input remaining + break; + } + if (((headerId == 0) && (dataSize == 0)) + || (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID)) { + // Ignore the field if it has to do with the old APK data alignment method (filling + // the extra field with 0x00 bytes) or the new APK data alignment method. + original.position(original.position() + dataSize); + continue; + } + // Copy this field (including header) to the output + original.position(original.position() - 4); + int originalLimit = original.limit(); + original.limit(original.position() + 4 + dataSize); + result.put(original); + original.limit(originalLimit); + } + + // Step 2. Add alignment field + // FORMAT: + // * uint16 extra header ID + // * uint16 extra data size + // Payload ('data size' bytes) + // * uint16 alignment multiple (in bytes) + // * remaining bytes -- padding to achieve alignment of data which starts after the + // extra field + long dataMinStartOffset = + extraStartOffset + result.position() + + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES; + int paddingSizeBytes = + (dataAlignmentMultiple - ((int) (dataMinStartOffset % dataAlignmentMultiple))) + % dataAlignmentMultiple; + result.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); + ZipUtils.putUnsignedInt16(result, 2 + paddingSizeBytes); + ZipUtils.putUnsignedInt16(result, dataAlignmentMultiple); + result.position(result.position() + paddingSizeBytes); + result.flip(); + + return result; + } + + private static ByteBuffer getZipCentralDirectory( + DataSource apk, + ApkUtils.ZipSections apkSections) throws IOException, ApkFormatException { + long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); + if (cdSizeBytes > Integer.MAX_VALUE) { + throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); + } + long cdOffset = apkSections.getZipCentralDirectoryOffset(); + ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); + cd.order(ByteOrder.LITTLE_ENDIAN); + return cd; + } + + private static List parseZipCentralDirectory( + ByteBuffer cd, + ApkUtils.ZipSections apkSections) throws ApkFormatException { + long cdOffset = apkSections.getZipCentralDirectoryOffset(); + int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); + List cdRecords = new ArrayList<>(expectedCdRecordCount); + Set entryNames = new HashSet<>(expectedCdRecordCount); + for (int i = 0; i < expectedCdRecordCount; i++) { + CentralDirectoryRecord cdRecord; + int offsetInsideCd = cd.position(); + try { + cdRecord = CentralDirectoryRecord.getRecord(cd); + } catch (ZipFormatException e) { + throw new ApkFormatException( + "Malformed ZIP Central Directory record #" + (i + 1) + + " at file offset " + (cdOffset + offsetInsideCd), + e); + } + String entryName = cdRecord.getName(); + if (!entryNames.add(entryName)) { + throw new ApkFormatException( + "Multiple ZIP entries with the same name: " + entryName); + } + cdRecords.add(cdRecord); + } + if (cd.hasRemaining()) { + throw new ApkFormatException( + "Unused space at the end of ZIP Central Directory: " + cd.remaining() + + " bytes starting at file offset " + (cdOffset + cd.position())); + } + + return cdRecords; + } + + private static CentralDirectoryRecord findCdRecord( + List cdRecords, String name) { + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (name.equals(cdRecord.getName())) { + return cdRecord; + } + } + return null; + } + + /** + * Returns the contents of the APK's {@code AndroidManifest.xml} or {@code null} if this entry + * is not present in the APK. + */ + static ByteBuffer getAndroidManifestFromApk( + List cdRecords, DataSource lhfSection) + throws IOException, ApkFormatException, ZipFormatException { + CentralDirectoryRecord androidManifestCdRecord = + findCdRecord(cdRecords, ANDROID_MANIFEST_ZIP_ENTRY_NAME); + if (androidManifestCdRecord == null) { + throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME); + } + + return ByteBuffer.wrap( + LocalFileRecord.getUncompressedData( + lhfSection, androidManifestCdRecord, lhfSection.size())); + } + + /** + * Return list of pin patterns embedded in the pin pattern asset + * file. If no such file, return {@code null}. + */ + private static List extractPinPatterns( + List cdRecords, DataSource lhfSection) + throws IOException, ApkFormatException { + CentralDirectoryRecord pinListCdRecord = + findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME); + List pinPatterns = null; + if (pinListCdRecord != null) { + pinPatterns = new ArrayList<>(); + byte[] patternBlob; + try { + patternBlob = LocalFileRecord.getUncompressedData( + lhfSection, pinListCdRecord, lhfSection.size()); + } catch (ZipFormatException ex) { + throw new ApkFormatException("Bad " + pinListCdRecord); + } + pinPatterns = Hints.parsePinPatterns(patternBlob); + } + return pinPatterns; + } + + /** + * Returns the minimum Android version (API Level) supported by the provided APK. This is based + * on the {@code android:minSdkVersion} attributes of the APK's {@code AndroidManifest.xml}. + */ + private static int getMinSdkVersionFromApk( + List cdRecords, DataSource lhfSection) + throws IOException, MinSdkVersionException { + ByteBuffer androidManifest; + try { + androidManifest = getAndroidManifestFromApk(cdRecords, lhfSection); + } catch (ZipFormatException | ApkFormatException e) { + throw new MinSdkVersionException( + "Failed to determine APK's minimum supported Android platform version", + e); + } + return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest); + } + + /** + * Configuration of a signer. + * + *

Use {@link Builder} to obtain configuration instances. + */ + public static class SignerConfig { + private final String mName; + private final PrivateKey mPrivateKey; + private final List mCertificates; + + private SignerConfig( + String name, + PrivateKey privateKey, + List certificates) { + mName = name; + mPrivateKey = privateKey; + mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates)); + } + + /** + * Returns the name of this signer. + */ + public String getName() { + return mName; + } + + /** + * Returns the signing key of this signer. + */ + public PrivateKey getPrivateKey() { + return mPrivateKey; + } + + /** + * Returns the certificate(s) of this signer. The first certificate's public key corresponds + * to this signer's private key. + */ + public List getCertificates() { + return mCertificates; + } + + /** + * Builder of {@link SignerConfig} instances. + */ + public static class Builder { + private final String mName; + private final PrivateKey mPrivateKey; + private final List mCertificates; + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + */ + public Builder( + String name, + PrivateKey privateKey, + List certificates) { + if (name.isEmpty()) { + throw new IllegalArgumentException("Empty name"); + } + mName = name; + mPrivateKey = privateKey; + mCertificates = new ArrayList<>(certificates); + } + + /** + * Returns a new {@code SignerConfig} instance configured based on the configuration of + * this builder. + */ + public SignerConfig build() { + return new SignerConfig( + mName, + mPrivateKey, + mCertificates); + } + } + } + + /** + * Builder of {@link ApkSigner} instances. + * + *

The builder requires the following information to construct a working {@code ApkSigner}: + *

    + *
  • Signer configs or {@link ApkSignerEngine} -- provided in the constructor,
  • + *
  • APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,
  • + *
  • where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk} + * variants. + *
  • + *
+ */ + public static class Builder { + private final List mSignerConfigs; + private boolean mV1SigningEnabled = true; + private boolean mV2SigningEnabled = true; + private boolean mV3SigningEnabled = true; + private boolean mDebuggableApkPermitted = true; + private boolean mOtherSignersSignaturesPreserved; + private String mCreatedBy; + private Integer mMinSdkVersion; + + private final ApkSignerEngine mSignerEngine; + + private File mInputApkFile; + private DataSource mInputApkDataSource; + + private File mOutputApkFile; + private DataSink mOutputApkDataSink; + private DataSource mOutputApkDataSource; + + private SigningCertificateLineage mSigningCertificateLineage; + + // APK Signature Scheme v3 only supports a single signing certificate, so to move to v3 + // signing by default, but not require prior clients to update to explicitly disable v3 + // signing for multiple signers, we modify the mV3SigningEnabled depending on the provided + // inputs (multiple signers and mSigningCertificateLineage in particular). Maintain two + // extra variables to record whether or not mV3SigningEnabled has been set directly by a + // client and so should override the default behavior. + private boolean mV3SigningExplicitlyDisabled = false; + private boolean mV3SigningExplicitlyEnabled = false; + + /** + * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided + * signer configurations. The resulting signer may be further customized through this + * builder's setters, such as {@link #setMinSdkVersion(int)}, + * {@link #setV1SigningEnabled(boolean)}, {@link #setV2SigningEnabled(boolean)}, + * {@link #setOtherSignersSignaturesPreserved(boolean)}, {@link #setCreatedBy(String)}. + * + *

{@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where + * more control over low-level details of signing is desired. + */ + public Builder(List signerConfigs) { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + if (signerConfigs.size() > 1) { + // APK Signature Scheme v3 only supports single signer, unless a + // SigningCertificateLineage is provided, in which case this will be reset to true, + // since we don't yet have a v4 scheme about which to worry + mV3SigningEnabled = false; + } + mSignerConfigs = new ArrayList<>(signerConfigs); + mSignerEngine = null; + } + + /** + * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the + * provided signing engine. This is meant for advanced use cases where more control is + * needed over the lower-level details of signing. For typical use cases, + * {@link #Builder(List)} is more appropriate. + */ + public Builder(ApkSignerEngine signerEngine) { + if (signerEngine == null) { + throw new NullPointerException("signerEngine == null"); + } + mSignerEngine = signerEngine; + mSignerConfigs = null; + } + + /** + * Sets the APK to be signed. + * + * @see #setInputApk(DataSource) + */ + public Builder setInputApk(File inputApk) { + if (inputApk == null) { + throw new NullPointerException("inputApk == null"); + } + mInputApkFile = inputApk; + mInputApkDataSource = null; + return this; + } + + /** + * Sets the APK to be signed. + * + * @see #setInputApk(File) + */ + public Builder setInputApk(DataSource inputApk) { + if (inputApk == null) { + throw new NullPointerException("inputApk == null"); + } + mInputApkDataSource = inputApk; + mInputApkFile = null; + return this; + } + + /** + * Sets the location of the output (signed) APK. {@code ApkSigner} will create this file if + * it doesn't exist. + * + * @see #setOutputApk(ReadableDataSink) + * @see #setOutputApk(DataSink, DataSource) + */ + public Builder setOutputApk(File outputApk) { + if (outputApk == null) { + throw new NullPointerException("outputApk == null"); + } + mOutputApkFile = outputApk; + mOutputApkDataSink = null; + mOutputApkDataSource = null; + return this; + } + + /** + * Sets the readable data sink which will receive the output (signed) APK. After signing, + * the contents of the output APK will be available via the {@link DataSource} interface of + * the sink. + * + *

This variant of {@code setOutputApk} is useful for avoiding writing the output APK to + * a file. For example, an in-memory data sink, such as + * {@link DataSinks#newInMemoryDataSink()}, could be used instead of a file. + * + * @see #setOutputApk(File) + * @see #setOutputApk(DataSink, DataSource) + */ + public Builder setOutputApk(ReadableDataSink outputApk) { + if (outputApk == null) { + throw new NullPointerException("outputApk == null"); + } + return setOutputApk(outputApk, outputApk); + } + + /** + * Sets the sink which will receive the output (signed) APK. Data received by the + * {@code outputApkOut} sink must be visible through the {@code outputApkIn} data source. + * + *

This is an advanced variant of {@link #setOutputApk(ReadableDataSink)}, enabling the + * sink and the source to be different objects. + * + * @see #setOutputApk(ReadableDataSink) + * @see #setOutputApk(File) + */ + public Builder setOutputApk(DataSink outputApkOut, DataSource outputApkIn) { + if (outputApkOut == null) { + throw new NullPointerException("outputApkOut == null"); + } + if (outputApkIn == null) { + throw new NullPointerException("outputApkIn == null"); + } + mOutputApkFile = null; + mOutputApkDataSink = outputApkOut; + mOutputApkDataSource = outputApkIn; + return this; + } + + /** + * Sets the minimum Android platform version (API Level) on which APK signatures produced + * by the signer being built must verify. This method is useful for overriding the default + * behavior where the minimum API Level is obtained from the {@code android:minSdkVersion} + * attribute of the APK's {@code AndroidManifest.xml}. + * + *

Note: This method may result in APK signatures which don't verify on some + * Android platform versions supported by the APK. + * + *

Note: This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an + * {@link ApkSignerEngine} + */ + public Builder setMinSdkVersion(int minSdkVersion) { + checkInitializedWithoutEngine(); + mMinSdkVersion = minSdkVersion; + return this; + } + + /** + * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme). + * + *

By default, whether APK is signed using JAR signing is determined by + * {@code ApkSigner}, based on the platform versions supported by the APK or specified using + * {@link #setMinSdkVersion(int)}. Disabling JAR signing will result in APK signatures which + * don't verify on Android Marshmallow (Android 6.0, API Level 23) and lower. + * + *

Note: This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @param enabled {@code true} to require the APK to be signed using JAR signing, + * {@code false} to require the APK to not be signed using JAR signing. + * + * @throws IllegalStateException if this builder was initialized with an + * {@link ApkSignerEngine} + * + * @see JAR signing + */ + public Builder setV1SigningEnabled(boolean enabled) { + checkInitializedWithoutEngine(); + mV1SigningEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature + * scheme). + * + *

By default, whether APK is signed using APK Signature Scheme v2 is determined by + * {@code ApkSigner} based on the platform versions supported by the APK or specified using + * {@link #setMinSdkVersion(int)}. + * + *

Note: This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme + * v2, {@code false} to require the APK to not be signed using APK Signature Scheme + * v2. + * + * @throws IllegalStateException if this builder was initialized with an + * {@link ApkSignerEngine} + * + * @see APK Signature Scheme v2 + */ + public Builder setV2SigningEnabled(boolean enabled) { + checkInitializedWithoutEngine(); + mV2SigningEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed using APK Signature Scheme v3 (aka v3 signature + * scheme). + * + *

By default, whether APK is signed using APK Signature Scheme v3 is determined by + * {@code ApkSigner} based on the platform versions supported by the APK or specified using + * {@link #setMinSdkVersion(int)}. + * + *

Note: This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + *

Note: APK Signature Scheme v3 only supports a single signing certificate, but + * may take multiple signers mapping to different targeted platform versions. + * + * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme + * v3, {@code false} to require the APK to not be signed using APK Signature Scheme + * v3. + * + * @throws IllegalStateException if this builder was initialized with an + * {@link ApkSignerEngine} + */ + public Builder setV3SigningEnabled(boolean enabled) { + checkInitializedWithoutEngine(); + mV3SigningEnabled = enabled; + if (enabled) { + mV3SigningExplicitlyEnabled = true; + } else { + mV3SigningExplicitlyDisabled = true; + } + return this; + } + + /** + * Sets whether the APK should be signed even if it is marked as debuggable + * ({@code android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward + * compatibility reasons, the default value of this setting is {@code true}. + * + *

It is dangerous to sign debuggable APKs with production/release keys because Android + * platform loosens security checks for such APKs. For example, arbitrary unauthorized code + * may be executed in the context of such an app by anybody with ADB shell access. + * + *

Note: This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + */ + public Builder setDebuggableApkPermitted(boolean permitted) { + checkInitializedWithoutEngine(); + mDebuggableApkPermitted = permitted; + return this; + } + + /** + * Sets whether signatures produced by signers other than the ones configured in this engine + * should be copied from the input APK to the output APK. + * + *

By default, signatures of other signers are omitted from the output APK. + * + *

Note: This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an + * {@link ApkSignerEngine} + */ + public Builder setOtherSignersSignaturesPreserved(boolean preserved) { + checkInitializedWithoutEngine(); + mOtherSignersSignaturesPreserved = preserved; + return this; + } + + /** + * Sets the value of the {@code Created-By} field in JAR signature files. + * + *

Note: This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an + * {@link ApkSignerEngine} + */ + public Builder setCreatedBy(String createdBy) { + checkInitializedWithoutEngine(); + if (createdBy == null) { + throw new NullPointerException(); + } + mCreatedBy = createdBy; + return this; + } + + private void checkInitializedWithoutEngine() { + if (mSignerEngine != null) { + throw new IllegalStateException( + "Operation is not available when builder initialized with an engine"); + } + } + + /** + * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This + * structure provides proof of signing certificate rotation linking {@link SignerConfig} + * objects to previous ones. + */ + public Builder setSigningCertificateLineage( + SigningCertificateLineage signingCertificateLineage) { + if (signingCertificateLineage != null) { + mV3SigningEnabled = true; + mSigningCertificateLineage = signingCertificateLineage; + } + return this; + } + + /** + * Returns a new {@code ApkSigner} instance initialized according to the configuration of + * this builder. + */ + public ApkSigner build() { + + if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) { + throw new IllegalStateException("Builder configured to both enable and disable APK " + + "Signature Scheme v3 signing"); + } + + if (mV3SigningExplicitlyDisabled) { + mV3SigningEnabled = false; + } + + if (mV3SigningExplicitlyEnabled) { + mV3SigningEnabled = true; + } + + // TODO - if v3 signing is enabled, check provided signers and history to see if valid + + return new ApkSigner( + mSignerConfigs, + mMinSdkVersion, + mV1SigningEnabled, + mV2SigningEnabled, + mV3SigningEnabled, + mDebuggableApkPermitted, + mOtherSignersSignaturesPreserved, + mCreatedBy, + mSignerEngine, + mInputApkFile, + mInputApkDataSource, + mOutputApkFile, + mOutputApkDataSink, + mOutputApkDataSource, + mSigningCertificateLineage); + } + } +} diff --git a/app/src/main/java/com/android/apksig/ApkSignerEngine.java b/app/src/main/java/com/android/apksig/ApkSignerEngine.java new file mode 100644 index 0000000..138bc38 --- /dev/null +++ b/app/src/main/java/com/android/apksig/ApkSignerEngine.java @@ -0,0 +1,520 @@ +/* + * 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.IOException; +import java.lang.UnsupportedOperationException; +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. + * + *

Operating Model

+ * + * 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. + * + *

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 + * Incremental Operation. + * + *

In the engine's operating model, a signed APK is produced as follows. + *

    + *
  1. JAR entries to be signed are output,
  2. + *
  3. JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the + * output,
  4. + *
  5. JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature + * to the output.
  6. + *
+ * + *

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. + * + *

To use the engine to sign an input APK (or a collection of JAR entries), follow these + * steps: + *

    + *
  1. Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used + * for signing multiple APKs.
  2. + *
  3. Locate the input APK's APK Signing Block and provide it to + * {@link #inputApkSigningBlock(DataSource)}.
  4. + *
  5. 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.
  6. + *
  7. For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to + * inspect the entry.
  8. + *
  9. 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.
  10. + *
  11. 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.
  12. + *
  13. Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will + * confirm that the output APK is signed.
  14. + *
  15. Invoke {@link #close()} to signal that the engine will no longer be used. This lets the + * engine free any resources it no longer needs. + *
+ * + *

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. + * + *

Incremental Operation

+ * + * 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. + * + *

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. + * + *

Output-only Operation

+ * + * 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 Application Signing + */ +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 initWith(byte[] manifestBytes, Set 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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; + + /** + * Indicates to this engine that it will no longer be used. Invoking this on an already closed + * engine is OK. + * + *

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. + * + *

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. + * + *

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. + * + *

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 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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(); + } +} diff --git a/app/src/main/java/com/android/apksig/ApkVerifier.java b/app/src/main/java/com/android/apksig/ApkVerifier.java new file mode 100644 index 0000000..3e1e7da --- /dev/null +++ b/app/src/main/java/com/android/apksig/ApkVerifier.java @@ -0,0 +1,1909 @@ +/* + * 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.apk.ApkUtils; +import com.android.apksig.internal.apk.AndroidBinXmlParser; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.v1.V1SchemeVerifier; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.v2.V2SchemeVerifier; +import com.android.apksig.internal.apk.v3.V3SchemeVerifier; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.zip.CentralDirectoryRecord; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; +import com.android.apksig.util.RunnablesExecutor; +import com.android.apksig.zip.ZipFormatException; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * APK signature verifier which mimics the behavior of the Android platform. + * + *

The verifier is designed to closely mimic the behavior of Android platforms. This is to enable + * the verifier to be used for checking whether an APK's signatures are expected to verify on + * Android. + * + *

Use {@link Builder} to obtain instances of this verifier. + * + * @see Application Signing + */ +public class ApkVerifier { + + private static final Map SUPPORTED_APK_SIG_SCHEME_NAMES = + loadSupportedApkSigSchemeNames(); + + private static Map loadSupportedApkSigSchemeNames() { + Map supportedMap = new HashMap<>(2); + supportedMap.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2"); + supportedMap.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, "APK Signature Scheme v3"); + return supportedMap; + } + + private final File mApkFile; + private final DataSource mApkDataSource; + + private final Integer mMinSdkVersion; + private final int mMaxSdkVersion; + + private ApkVerifier( + File apkFile, + DataSource apkDataSource, + Integer minSdkVersion, + int maxSdkVersion) { + mApkFile = apkFile; + mApkDataSource = apkDataSource; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + } + + /** + * Verifies the APK's signatures and returns the result of verification. The APK can be + * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. + * The verification result also includes errors, warnings, and information about signers such + * as their signing certificates. + * + *

Verification succeeds iff the APK's signature is expected to verify on all Android + * platform versions specified via the {@link Builder}. 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 {@link Result#isVerified()} returns {@code false}, or this method + * throws an exception. + * + * @throws IOException if an I/O error is encountered while reading the APK + * @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 IllegalStateException if this verifier's configuration is missing required + * information. + */ + public Result verify() throws IOException, ApkFormatException, NoSuchAlgorithmException, + IllegalStateException { + 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 verify(apk); + } finally { + if (in != null) { + in.close(); + } + } + } + + /** + * Verifies the APK's signatures and returns the result of verification. The APK can be + * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. + * The verification result also includes errors, warnings, and information about signers. + * + * @param apk APK file contents + * + * @throws IOException if an I/O error is encountered while reading the APK + * @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 + */ + private Result verify(DataSource apk) + throws IOException, ApkFormatException, NoSuchAlgorithmException { + if (mMinSdkVersion != null) { + if (mMinSdkVersion < 0) { + throw new IllegalArgumentException( + "minSdkVersion must not be negative: " + mMinSdkVersion); + } + if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) { + throw new IllegalArgumentException( + "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion + + ")"); + } + } + int maxSdkVersion = mMaxSdkVersion; + + ApkUtils.ZipSections zipSections; + try { + zipSections = ApkUtils.findZipSections(apk); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed APK: not a ZIP archive", e); + } + + ByteBuffer androidManifest = null; + + int minSdkVersion; + if (mMinSdkVersion != null) { + // No need to obtain minSdkVersion from the APK's AndroidManifest.xml + minSdkVersion = mMinSdkVersion; + } else { + // Need to obtain minSdkVersion from the APK's AndroidManifest.xml + if (androidManifest == null) { + androidManifest = getAndroidManifestFromApk(apk, zipSections); + } + minSdkVersion = + ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice()); + if (minSdkVersion > mMaxSdkVersion) { + throw new IllegalArgumentException( + "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion (" + + mMaxSdkVersion + ")"); + } + } + + Result result = new Result(); + + // The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme + // name, but the verifiers use this parameter as the schemes supported by the target SDK + // range. Since the code below skips signature verification based on max SDK the mapping of + // supported schemes needs to be modified to ensure the verifiers do not report a stripped + // signature for an SDK range that does not support that signature version. For instance an + // APK with V1, V2, and V3 signatures and a max SDK of O would skip the V3 signature + // verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2 + // verification is performed it would see the stripping protection attribute, see that V3 + // is in the list of supported signatures, and report a stripped signature. + Map supportedSchemeNames; + if (maxSdkVersion >= AndroidSdkVersion.P) { + supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES; + } else if (maxSdkVersion >= AndroidSdkVersion.N) { + supportedSchemeNames = new HashMap<>(1); + supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, + SUPPORTED_APK_SIG_SCHEME_NAMES.get( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2)); + } else { + supportedSchemeNames = Collections.EMPTY_MAP; + } + // Android N and newer attempts to verify APKs using the APK Signing Block, which can + // include v2 and/or v3 signatures. If none is found, it falls back to JAR signature + // verification. If the signature is found but does not verify, the APK is rejected. + Set foundApkSigSchemeIds = new HashSet<>(2); + if (maxSdkVersion >= AndroidSdkVersion.N) { + RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED; + // Android P and newer attempts to verify APKs using APK Signature Scheme v3 + if (maxSdkVersion >= AndroidSdkVersion.P) { + try { + ApkSigningBlockUtils.Result v3Result = + V3SchemeVerifier.verify( + executor, + apk, + zipSections, + Math.max(minSdkVersion, AndroidSdkVersion.P), + maxSdkVersion); + foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + result.mergeFrom(v3Result); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // v3 signature not required + } + if (result.containsErrors()) { + return result; + } + } + + // Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P + // ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or + // APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if + // no APK Signature Scheme v3 (or newer scheme) signatures were found. + if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) { + try { + ApkSigningBlockUtils.Result v2Result = + V2SchemeVerifier.verify( + executor, + apk, + zipSections, + supportedSchemeNames, + foundApkSigSchemeIds, + Math.max(minSdkVersion, AndroidSdkVersion.N), + maxSdkVersion); + foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); + result.mergeFrom(v2Result); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // v2 signature not required + } + if (result.containsErrors()) { + return result; + } + } + } + + // Android O and newer requires that APKs targeting security sandbox version 2 and higher + // are signed using APK Signature Scheme v2 or newer. + if (maxSdkVersion >= AndroidSdkVersion.O) { + if (androidManifest == null) { + androidManifest = getAndroidManifestFromApk(apk, zipSections); + } + int targetSandboxVersion = + getTargetSandboxVersionFromBinaryAndroidManifest(androidManifest.slice()); + if (targetSandboxVersion > 1) { + if (foundApkSigSchemeIds.isEmpty()) { + result.addError( + Issue.NO_SIG_FOR_TARGET_SANDBOX_VERSION, + targetSandboxVersion); + } + } + } + + // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N + // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures. + // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer + // scheme) signatures were found. + if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) { + V1SchemeVerifier.Result v1Result = + V1SchemeVerifier.verify( + apk, + zipSections, + supportedSchemeNames, + foundApkSigSchemeIds, + minSdkVersion, + maxSdkVersion); + result.mergeFrom(v1Result); + } + if (result.containsErrors()) { + return result; + } + + // Check whether v1 and v2 scheme signer identifies match, provided both v1 and v2 + // signatures verified. + if ((result.isVerifiedUsingV1Scheme()) && (result.isVerifiedUsingV2Scheme())) { + ArrayList v1Signers = + new ArrayList<>(result.getV1SchemeSigners()); + ArrayList v2Signers = + new ArrayList<>(result.getV2SchemeSigners()); + ArrayList v1SignerCerts = new ArrayList<>(); + ArrayList v2SignerCerts = new ArrayList<>(); + for (Result.V1SchemeSignerInfo signer : v1Signers) { + try { + v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded())); + } catch (CertificateEncodingException e) { + throw new RuntimeException( + "Failed to encode JAR signer " + signer.getName() + " certs", e); + } + } + for (Result.V2SchemeSignerInfo signer : v2Signers) { + try { + v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded())); + } catch (CertificateEncodingException e) { + throw new RuntimeException( + "Failed to encode APK Signature Scheme v2 signer (index: " + + signer.getIndex() + ") certs", + e); + } + } + + for (int i = 0; i < v1SignerCerts.size(); i++) { + ByteArray v1Cert = v1SignerCerts.get(i); + if (!v2SignerCerts.contains(v1Cert)) { + Result.V1SchemeSignerInfo v1Signer = v1Signers.get(i); + v1Signer.addError(Issue.V2_SIG_MISSING); + break; + } + } + for (int i = 0; i < v2SignerCerts.size(); i++) { + ByteArray v2Cert = v2SignerCerts.get(i); + if (!v1SignerCerts.contains(v2Cert)) { + Result.V2SchemeSignerInfo v2Signer = v2Signers.get(i); + v2Signer.addError(Issue.JAR_SIG_MISSING); + break; + } + } + } + + // If there is a v3 scheme signer and an earlier scheme signer, make sure that there is a + // match, or in the event of signing certificate rotation, that the v1/v2 scheme signer + // matches the oldest signing certificate in the provided SigningCertificateLineage + if (result.isVerifiedUsingV3Scheme() + && (result.isVerifiedUsingV1Scheme() || result.isVerifiedUsingV2Scheme())) { + SigningCertificateLineage lineage = result.getSigningCertificateLineage(); + X509Certificate oldSignerCert; + if (result.isVerifiedUsingV1Scheme()) { + List v1Signers = result.getV1SchemeSigners(); + if (v1Signers.size() != 1) { + // APK Signature Scheme v3 only supports single-signers, error to sign with + // multiple and then only one + result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS); + } + oldSignerCert = v1Signers.get(0).mCertChain.get(0); + } else { + List v2Signers = result.getV2SchemeSigners(); + if (v2Signers.size() != 1) { + // APK Signature Scheme v3 only supports single-signers, error to sign with + // multiple and then only one + result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS); + } + oldSignerCert = v2Signers.get(0).mCerts.get(0); + } + if (lineage == null) { + // no signing certificate history with which to contend, just make sure that v3 + // matches previous versions + List v3Signers = result.getV3SchemeSigners(); + if (v3Signers.size() != 1) { + // multiple v3 signers should never exist without rotation history, since + // multiple signers implies a different signer for different platform versions + result.addError(Issue.V3_SIG_MULTIPLE_SIGNERS); + } + try { + if (!Arrays.equals(oldSignerCert.getEncoded(), + v3Signers.get(0).mCerts.get(0).getEncoded())) { + result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH); + } + } catch (CertificateEncodingException e) { + // we just go the encoding for the v1/v2 certs above, so must be v3 + throw new RuntimeException( + "Failed to encode APK Signature Scheme v3 signer cert", e); + } + } else { + // we have some signing history, make sure that the root of the history is the same + // as our v1/v2 signer + try { + lineage = lineage.getSubLineage(oldSignerCert); + if (lineage.size() != 1) { + // the v1/v2 signer was found, but not at the root of the lineage + result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH); + } + } catch (IllegalArgumentException e) { + // the v1/v2 signer was not found in the lineage + result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH); + } + } + } + + if (result.containsErrors()) { + return result; + } + + // Verified + result.setVerified(); + if (result.isVerifiedUsingV3Scheme()) { + List v3Signers = result.getV3SchemeSigners(); + result.addSignerCertificate(v3Signers.get(v3Signers.size() - 1).getCertificate()); + } else if (result.isVerifiedUsingV2Scheme()) { + for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) { + result.addSignerCertificate(signerInfo.getCertificate()); + } + } else if (result.isVerifiedUsingV1Scheme()) { + for (Result.V1SchemeSignerInfo signerInfo : result.getV1SchemeSigners()) { + result.addSignerCertificate(signerInfo.getCertificate()); + } + } else { + throw new RuntimeException( + "APK verified, but has not verified using any of v1, v2 or v3schemes"); + } + + return result; + } + + private static ByteBuffer getAndroidManifestFromApk( + DataSource apk, ApkUtils.ZipSections zipSections) + throws IOException, ApkFormatException { + List cdRecords = + V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); + try { + return ApkSigner.getAndroidManifestFromApk( + cdRecords, + apk.slice(0, zipSections.getZipCentralDirectoryOffset())); + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read AndroidManifest.xml", e); + } + } + + /** + * Android resource ID of the {@code android:targetSandboxVersion} attribute in + * AndroidManifest.xml. + */ + private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c; + + /** + * Returns the security sandbox version targeted by an APK with the provided + * {@code AndroidManifest.xml}. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + * + * @throws ApkFormatException if an error occurred while determining the version + */ + private static int getTargetSandboxVersionFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) throws ApkFormatException { + // Return the value of the android:targetSandboxVersion attribute of the top-level manifest + // element + 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())) { + // In each manifest element, targetSandboxVersion defaults to 1 + int result = 1; + for (int i = 0; i < parser.getAttributeCount(); i++) { + if (parser.getAttributeNameResourceId(i) + == TARGET_SANDBOX_VERSION_ATTR_ID) { + int valueType = parser.getAttributeValueType(i); + switch (valueType) { + case AndroidBinXmlParser.VALUE_TYPE_INT: + result = parser.getAttributeIntValue(i); + break; + default: + throw new ApkFormatException( + "Failed to determine APK's target sandbox version" + + ": unsupported value type of" + + " AndroidManifest.xml" + + " android:targetSandboxVersion" + + ". Only integer values supported."); + } + break; + } + } + return result; + } + eventType = parser.next(); + } + throw new ApkFormatException( + "Failed to determine APK's target sandbox version" + + " : no manifest element in AndroidManifest.xml"); + } catch (AndroidBinXmlParser.XmlParserException e) { + throw new ApkFormatException( + "Failed to determine APK's target sandbox version" + + ": malformed AndroidManifest.xml", + e); + } + } + + /** + * Result of verifying an APKs signatures. The APK can be considered verified iff + * {@link #isVerified()} returns {@code true}. + */ + public static class Result { + private final List mErrors = new ArrayList<>(); + private final List mWarnings = new ArrayList<>(); + private final List mSignerCerts = new ArrayList<>(); + private final List mV1SchemeSigners = new ArrayList<>(); + private final List mV1SchemeIgnoredSigners = new ArrayList<>(); + private final List mV2SchemeSigners = new ArrayList<>(); + private final List mV3SchemeSigners = new ArrayList<>(); + + private boolean mVerified; + private boolean mVerifiedUsingV1Scheme; + private boolean mVerifiedUsingV2Scheme; + private boolean mVerifiedUsingV3Scheme; + private SigningCertificateLineage mSigningCertificateLineage; + + /** + * Returns {@code true} if the APK's signatures verified. + */ + public boolean isVerified() { + return mVerified; + } + + private void setVerified() { + mVerified = true; + } + + /** + * Returns {@code true} if the APK's JAR signatures verified. + */ + public boolean isVerifiedUsingV1Scheme() { + return mVerifiedUsingV1Scheme; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified. + */ + public boolean isVerifiedUsingV2Scheme() { + return mVerifiedUsingV2Scheme; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v3 signature verified. + */ + public boolean isVerifiedUsingV3Scheme() { + return mVerifiedUsingV3Scheme; + } + + /** + * Returns the verified signers' certificates, one per signer. + */ + public List getSignerCertificates() { + return mSignerCerts; + } + + private void addSignerCertificate(X509Certificate cert) { + mSignerCerts.add(cert); + } + + /** + * Returns information about JAR signers associated with the APK's signature. These are the + * signers used by Android. + * + * @see #getV1SchemeIgnoredSigners() + */ + public List getV1SchemeSigners() { + return mV1SchemeSigners; + } + + /** + * Returns information about JAR signers ignored by the APK's signature verification + * process. These signers are ignored by Android. However, each signer's errors or warnings + * will contain information about why they are ignored. + * + * @see #getV1SchemeSigners() + */ + public List getV1SchemeIgnoredSigners() { + return mV1SchemeIgnoredSigners; + } + + /** + * Returns information about APK Signature Scheme v2 signers associated with the APK's + * signature. + */ + public List getV2SchemeSigners() { + return mV2SchemeSigners; + } + + /** + * Returns information about APK Signature Scheme v3 signers associated with the APK's + * signature. + * + * Multiple signers represent different targeted platform versions, not + * a signing identity of multiple signers. APK Signature Scheme v3 only supports single + * signer identities. + */ + public List getV3SchemeSigners() { + return mV3SchemeSigners; + } + + /** + * Returns the combined SigningCertificateLineage associated with this APK's APK Signature + * Scheme v3 signing block. + */ + public SigningCertificateLineage getSigningCertificateLineage() { + return mSigningCertificateLineage; + } + + void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + /** + * Returns errors encountered while verifying the APK's signatures. + */ + public List getErrors() { + return mErrors; + } + + /** + * Returns warnings encountered while verifying the APK's signatures. + */ + public List getWarnings() { + return mWarnings; + } + + private void mergeFrom(V1SchemeVerifier.Result source) { + mVerifiedUsingV1Scheme = source.verified; + mErrors.addAll(source.getErrors()); + mWarnings.addAll(source.getWarnings()); + for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) { + mV1SchemeSigners.add(new V1SchemeSignerInfo(signer)); + } + for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) { + mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer)); + } + } + + private void mergeFrom(ApkSigningBlockUtils.Result source) { + switch (source.signatureSchemeVersion) { + case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2: + mVerifiedUsingV2Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV2SchemeSigners.add(new V2SchemeSignerInfo(signer)); + } + break; + case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3: + mVerifiedUsingV3Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV3SchemeSigners.add(new V3SchemeSignerInfo(signer)); + } + mSigningCertificateLineage = source.signingCertificateLineage; + break; + default: + throw new IllegalArgumentException("Unknown Signing Block Scheme Id"); + } + mErrors.addAll(source.getErrors()); + mWarnings.addAll(source.getWarnings()); + } + + /** + * Returns {@code true} if an error was encountered while verifying the APK. Any error + * prevents the APK from being considered verified. + */ + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + if (!mV1SchemeSigners.isEmpty()) { + for (V1SchemeSignerInfo signer : mV1SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + } + } + if (!mV2SchemeSigners.isEmpty()) { + for (V2SchemeSignerInfo signer : mV2SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + } + } + if (!mV3SchemeSigners.isEmpty()) { + for (V3SchemeSignerInfo signer : mV3SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + } + } + + return false; + } + + /** + * Information about a JAR signer associated with the APK's signature. + */ + public static class V1SchemeSignerInfo { + private final String mName; + private final List mCertChain; + private final String mSignatureBlockFileName; + private final String mSignatureFileName; + + private final List mErrors; + private final List mWarnings; + + private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) { + mName = result.name; + mCertChain = result.certChain; + mSignatureBlockFileName = result.signatureBlockFileName; + mSignatureFileName = result.signatureFileName; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + } + + /** + * Returns a user-friendly name of the signer. + */ + public String getName() { + return mName; + } + + /** + * Returns the name of the JAR entry containing this signer's JAR signature block file. + */ + public String getSignatureBlockFileName() { + return mSignatureBlockFileName; + } + + /** + * Returns the name of the JAR entry containing this signer's JAR signature file. + */ + public String getSignatureFileName() { + return mSignatureFileName; + } + + /** + * Returns this signer'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()}. + * + *

This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCertChain.isEmpty() ? null : mCertChain.get(0); + } + + /** + * Returns the certificate chain for the signer's public key. The certificate containing + * the public key is first, followed by the certificate (if any) which issued the + * signing certificate, and so forth. An empty list may be returned if an error was + * encountered during verification (see {@link #containsErrors()}). + */ + public List getCertificateChain() { + return mCertChain; + } + + /** + * Returns {@code true} if an error was encountered while verifying this signer's JAR + * signature. Any error prevents the signer's signature from being considered verified. + */ + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + /** + * Returns errors encountered while verifying this signer's JAR signature. Any error + * prevents the signer's signature from being considered verified. + */ + public List getErrors() { + return mErrors; + } + + /** + * Returns warnings encountered while verifying this signer's JAR signature. Warnings + * do not prevent the signer's signature from being considered verified. + */ + public List getWarnings() { + return mWarnings; + } + + private void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + } + + /** + * Information about an APK Signature Scheme v2 signer associated with the APK's signature. + */ + public static class V2SchemeSignerInfo { + private final int mIndex; + private final List mCerts; + + private final List mErrors; + private final List mWarnings; + + private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { + mIndex = result.index; + mCerts = result.certs; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + } + + /** + * Returns this signer's {@code 0}-based index in the list of signers contained in the + * APK's APK Signature Scheme v2 signature. + */ + public int getIndex() { + return mIndex; + } + + /** + * Returns this signer'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()}. + * + *

This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCerts.isEmpty() ? null : mCerts.get(0); + } + + /** + * Returns this signer's certificates. The first certificate is for the signer's public + * key. An empty list may be returned if an error was encountered during verification + * (see {@link #containsErrors()}). + */ + public List getCertificates() { + return mCerts; + } + + private void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + } + + /** + * Information about an APK Signature Scheme v3 signer associated with the APK's signature. + */ + public static class V3SchemeSignerInfo { + private final int mIndex; + private final List mCerts; + + private final List mErrors; + private final List mWarnings; + + private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { + mIndex = result.index; + mCerts = result.certs; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + } + + /** + * Returns this signer's {@code 0}-based index in the list of signers contained in the + * APK's APK Signature Scheme v3 signature. + */ + public int getIndex() { + return mIndex; + } + + /** + * Returns this signer'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()}. + * + *

This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCerts.isEmpty() ? null : mCerts.get(0); + } + + /** + * Returns this signer's certificates. The first certificate is for the signer's public + * key. An empty list may be returned if an error was encountered during verification + * (see {@link #containsErrors()}). + */ + public List getCertificates() { + return mCerts; + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + } + } + + /** + * Error or warning encountered while verifying an APK's signatures. + */ + public static enum Issue { + + /** + * APK is not JAR-signed. + */ + JAR_SIG_NO_SIGNATURES("No JAR signatures"), + + /** + * APK does not contain any entries covered by JAR signatures. + */ + JAR_SIG_NO_SIGNED_ZIP_ENTRIES("No JAR entries covered by JAR signatures"), + + /** + * APK contains multiple entries with the same name. + * + *

    + *
  • Parameter 1: name ({@code String})
  • + *
+ */ + JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"), + + /** + * JAR manifest contains a section with a duplicate name. + * + *
    + *
  • Parameter 1: section name ({@code String})
  • + *
+ */ + JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"), + + /** + * JAR manifest contains a section without a name. + * + *
    + *
  • Parameter 1: section index (1-based) ({@code Integer})
  • + *
+ */ + JAR_SIG_UNNNAMED_MANIFEST_SECTION( + "Malformed META-INF/MANIFEST.MF: invidual section #%1$d does not have a name"), + + /** + * JAR signature file contains a section without a name. + * + *
    + *
  • Parameter 1: signature file name ({@code String})
  • + *
  • Parameter 2: section index (1-based) ({@code Integer})
  • + *
+ */ + JAR_SIG_UNNNAMED_SIG_FILE_SECTION( + "Malformed %1$s: invidual section #%2$d does not have a name"), + + /** APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). */ + JAR_SIG_NO_MANIFEST("Missing META-INF/MANIFEST.MF"), + + /** + * JAR manifest references an entry which is not there in the APK. + * + *
    + *
  • Parameter 1: entry name ({@code String})
  • + *
+ */ + JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST( + "%1$s entry referenced by META-INF/MANIFEST.MF not found in the APK"), + + /** + * JAR manifest does not list a digest for the specified entry. + * + *
    + *
  • Parameter 1: entry name ({@code String})
  • + *
+ */ + JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST("No digest for %1$s in META-INF/MANIFEST.MF"), + + /** + * JAR signature does not list a digest for the specified entry. + * + *
    + *
  • Parameter 1: entry name ({@code String})
  • + *
  • Parameter 2: signature file name ({@code String})
  • + *
+ */ + JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE("No digest for %1$s in %2$s"), + + /** + * The specified JAR entry is not covered by JAR signature. + * + *
    + *
  • Parameter 1: entry name ({@code String})
  • + *
+ */ + JAR_SIG_ZIP_ENTRY_NOT_SIGNED("%1$s entry not signed"), + + /** + * JAR signature uses different set of signers to protect the two specified ZIP entries. + * + *
    + *
  • Parameter 1: first entry name ({@code String})
  • + *
  • Parameter 2: first entry signer names ({@code List})
  • + *
  • Parameter 3: second entry name ({@code String})
  • + *
  • Parameter 4: second entry signer names ({@code List})
  • + *
+ */ + JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH( + "Entries %1$s and %3$s are signed with different sets of signers" + + " : <%2$s> vs <%4$s>"), + + /** + * Digest of the specified ZIP entry's data does not match the digest expected by the JAR + * signature. + * + *
    + *
  • Parameter 1: entry name ({@code String})
  • + *
  • Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})
  • + *
  • Parameter 3: name of the entry in which the expected digest is specified + * ({@code String})
  • + *
  • Parameter 4: base64-encoded actual digest ({@code String})
  • + *
  • Parameter 5: base64-encoded expected digest ({@code String})
  • + *
+ */ + JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY( + "%2$s digest of %1$s does not match the digest specified in %3$s" + + ". Expected: <%5$s>, actual: <%4$s>"), + + /** + * Digest of the JAR manifest main section did not verify. + * + *
    + *
  • Parameter 1: digest algorithm (e.g., SHA-256) ({@code String})
  • + *
  • Parameter 2: name of the entry in which the expected digest is specified + * ({@code String})
  • + *
  • Parameter 3: base64-encoded actual digest ({@code String})
  • + *
  • Parameter 4: base64-encoded expected digest ({@code String})
  • + *
+ */ + JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY( + "%1$s digest of META-INF/MANIFEST.MF main section does not match the digest" + + " specified in %2$s. Expected: <%4$s>, actual: <%3$s>"), + + /** + * Digest of the specified JAR manifest section does not match the digest expected by the + * JAR signature. + * + *
    + *
  • Parameter 1: section name ({@code String})
  • + *
  • Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})
  • + *
  • Parameter 3: name of the signature file in which the expected digest is specified + * ({@code String})
  • + *
  • Parameter 4: base64-encoded actual digest ({@code String})
  • + *
  • Parameter 5: base64-encoded expected digest ({@code String})
  • + *
+ */ + JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY( + "%2$s digest of META-INF/MANIFEST.MF section for %1$s does not match the digest" + + " specified in %3$s. Expected: <%5$s>, actual: <%4$s>"), + + /** + * JAR signature file does not contain the whole-file digest of the JAR manifest file. The + * digest speeds up verification of JAR signature. + * + *
    + *
  • Parameter 1: name of the signature file ({@code String})
  • + *
+ */ + JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE( + "%1$s does not specify digest of META-INF/MANIFEST.MF" + + ". This slows down verification."), + + /** + * APK is signed using APK Signature Scheme v2 or newer, but JAR signature file does not + * contain protections against stripping of these newer scheme signatures. + * + *
    + *
  • Parameter 1: name of the signature file ({@code String})
  • + *
+ */ + JAR_SIG_NO_APK_SIG_STRIP_PROTECTION( + "APK is signed using APK Signature Scheme v2 but these signatures may be stripped" + + " without being detected because %1$s does not contain anti-stripping" + + " protections."), + + /** + * JAR signature of the signer is missing a file/entry. + * + *
    + *
  • Parameter 1: name of the encountered file ({@code String})
  • + *
  • Parameter 2: name of the missing file ({@code String})
  • + *
+ */ + JAR_SIG_MISSING_FILE("Partial JAR signature. Found: %1$s, missing: %2$s"), + + /** + * An exception was encountered while verifying JAR signature contained in a signature block + * against the signature file. + * + *
    + *
  • Parameter 1: name of the signature block file ({@code String})
  • + *
  • Parameter 2: name of the signature file ({@code String})
  • + *
  • Parameter 3: exception ({@code Throwable})
  • + *
+ */ + JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"), + + /** + * JAR signature contains unsupported digest algorithm. + * + *
    + *
  • Parameter 1: name of the signature block file ({@code String})
  • + *
  • Parameter 2: digest algorithm OID ({@code String})
  • + *
  • Parameter 3: signature algorithm OID ({@code String})
  • + *
  • Parameter 4: API Levels on which this combination of algorithms is not supported + * ({@code String})
  • + *
  • Parameter 5: user-friendly variant of digest algorithm ({@code String})
  • + *
  • Parameter 6: user-friendly variant of signature algorithm ({@code String})
  • + *
+ */ + JAR_SIG_UNSUPPORTED_SIG_ALG( + "JAR signature %1$s uses digest algorithm %5$s and signature algorithm %6$s which" + + " is not supported on API Level(s) %4$s for which this APK is being" + + " verified"), + + /** + * An exception was encountered while parsing JAR signature contained in a signature block. + * + *
    + *
  • Parameter 1: name of the signature block file ({@code String})
  • + *
  • Parameter 2: exception ({@code Throwable})
  • + *
+ */ + JAR_SIG_PARSE_EXCEPTION("Failed to parse JAR signature %1$s: %2$s"), + + /** + * An exception was encountered while parsing a certificate contained in the JAR signature + * block. + * + *
    + *
  • Parameter 1: name of the signature block file ({@code String})
  • + *
  • Parameter 2: exception ({@code Throwable})
  • + *
+ */ + JAR_SIG_MALFORMED_CERTIFICATE("Malformed certificate in JAR signature %1$s: %2$s"), + + /** + * JAR signature contained in a signature block file did not verify against the signature + * file. + * + *
    + *
  • Parameter 1: name of the signature block file ({@code String})
  • + *
  • Parameter 2: name of the signature file ({@code String})
  • + *
+ */ + JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"), + + /** + * JAR signature contains no verified signers. + * + *
    + *
  • Parameter 1: name of the signature block file ({@code String})
  • + *
+ */ + JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"), + + /** + * JAR signature file contains a section with a duplicate name. + * + *
    + *
  • Parameter 1: signature file name ({@code String})
  • + *
  • Parameter 1: section name ({@code String})
  • + *
+ */ + JAR_SIG_DUPLICATE_SIG_FILE_SECTION("Duplicate section in %1$s: %2$s"), + + /** + * JAR signature file's main section doesn't contain the mandatory Signature-Version + * attribute. + * + *
    + *
  • Parameter 1: signature file name ({@code String})
  • + *
+ */ + JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE( + "Malformed %1$s: missing Signature-Version attribute"), + + /** + * JAR signature file references an unknown APK signature scheme ID. + * + *
    + *
  • Parameter 1: name of the signature file ({@code String})
  • + *
  • Parameter 2: unknown APK signature scheme ID ({@code} Integer)
  • + *
+ */ + JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID( + "JAR signature %1$s references unknown APK signature scheme ID: %2$d"), + + /** + * JAR signature file indicates that the APK is supposed to be signed with a supported APK + * signature scheme (in addition to the JAR signature) but no such signature was found in + * the APK. + * + *
    + *
  • Parameter 1: name of the signature file ({@code String})
  • + *
  • Parameter 2: APK signature scheme ID ({@code} Integer)
  • + *
  • Parameter 3: APK signature scheme English name ({@code} String)
  • + *
+ */ + JAR_SIG_MISSING_APK_SIG_REFERENCED( + "JAR signature %1$s indicates the APK is signed using %3$s but no such signature" + + " was found. Signature stripped?"), + + /** + * JAR entry is not covered by signature and thus unauthorized modifications to its contents + * will not be detected. + * + *
    + *
  • Parameter 1: entry name ({@code String})
  • + *
+ */ + JAR_SIG_UNPROTECTED_ZIP_ENTRY( + "%1$s not protected by signature. Unauthorized modifications to this JAR entry" + + " will not be detected. Delete or move the entry outside of META-INF/."), + + /** + * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains an APK + * Signature Scheme v2 signature from this signer, but does not contain a JAR signature + * from this signer. + */ + JAR_SIG_MISSING("No JAR signature from this signer"), + + /** + * APK is targeting a sandbox version which requires APK Signature Scheme v2 signature but + * no such signature was found. + * + *
    + *
  • Parameter 1: target sandbox version ({@code Integer})
  • + *
+ */ + NO_SIG_FOR_TARGET_SANDBOX_VERSION( + "Missing APK Signature Scheme v2 signature required for target sandbox version" + + " %1$d"), + + /** + * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR + * signature from this signer, but does not contain an APK Signature Scheme v2 signature + * from this signer. + */ + V2_SIG_MISSING("No APK Signature Scheme v2 signature from this signer"), + + /** + * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature. + */ + V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"), + + /** + * Failed to parse this signer's signer block contained in the APK Signature Scheme v2 + * signature. + */ + V2_SIG_MALFORMED_SIGNER("Malformed signer block"), + + /** + * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be + * parsed. + * + *
    + *
  • Parameter 1: error details ({@code Throwable})
  • + *
+ */ + V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), + + /** + * This APK Signature Scheme v2 signer's certificate could not be parsed. + * + *
    + *
  • Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of + * certificates ({@code Integer})
  • + *
  • Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's + * list of certificates ({@code Integer})
  • + *
  • Parameter 3: error details ({@code Throwable})
  • + *
+ */ + V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"), + + /** + * Failed to parse this signer's signature record contained in the APK Signature Scheme v2 + * signature. + * + *
    + *
  • Parameter 1: record number (first record is {@code 1}) ({@code Integer})
  • + *
+ */ + V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"), + + /** + * Failed to parse this signer's digest record contained in the APK Signature Scheme v2 + * signature. + * + *
    + *
  • Parameter 1: record number (first record is {@code 1}) ({@code Integer})
  • + *
+ */ + V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"), + + /** + * This APK Signature Scheme v2 signer contains a malformed additional attribute. + * + *
    + *
  • Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})
  • + *
+ */ + V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"), + + /** + * APK Signature Scheme v2 signature references an unknown APK signature scheme ID. + * + *
    + *
  • Parameter 1: signer index ({@code Integer})
  • + *
  • Parameter 2: unknown APK signature scheme ID ({@code} Integer)
  • + *
+ */ + V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID( + "APK Signature Scheme v2 signer: %1$s references unknown APK signature scheme ID: " + + "%2$d"), + + /** + * APK Signature Scheme v2 signature indicates that the APK is supposed to be signed with a + * supported APK signature scheme (in addition to the v2 signature) but no such signature + * was found in the APK. + * + *
    + *
  • Parameter 1: signer index ({@code Integer})
  • + *
  • Parameter 2: APK signature scheme English name ({@code} String)
  • + *
+ */ + V2_SIG_MISSING_APK_SIG_REFERENCED( + "APK Signature Scheme v2 signature %1$s indicates the APK is signed using %2$s but " + + "no such signature was found. Signature stripped?"), + + /** + * APK Signature Scheme v2 signature contains no signers. + */ + V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"), + + /** + * This APK Signature Scheme v2 signer contains a signature produced using an unknown + * algorithm. + * + *
    + *
  • Parameter 1: algorithm ID ({@code Integer})
  • + *
+ */ + V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), + + /** + * This APK Signature Scheme v2 signer contains an unknown additional attribute. + * + *
    + *
  • Parameter 1: attribute ID ({@code Integer})
  • + *
+ */ + V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"), + + /** + * An exception was encountered while verifying APK Signature Scheme v2 signature of this + * signer. + * + *
    + *
  • Parameter 1: signature algorithm ({@link SignatureAlgorithm})
  • + *
  • Parameter 2: exception ({@code Throwable})
  • + *
+ */ + V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), + + /** + * APK Signature Scheme v2 signature over this signer's signed-data block did not verify. + * + *
    + *
  • Parameter 1: signature algorithm ({@link SignatureAlgorithm})
  • + *
+ */ + V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), + + /** + * This APK Signature Scheme v2 signer offers no signatures. + */ + V2_SIG_NO_SIGNATURES("No signatures"), + + /** + * This APK Signature Scheme v2 signer offers signatures but none of them are supported. + */ + V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"), + + /** + * This APK Signature Scheme v2 signer offers no certificates. + */ + V2_SIG_NO_CERTIFICATES("No certificates"), + + /** + * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does + * not match the public key listed in the signatures record. + * + *
    + *
  • Parameter 1: hex-encoded public key from certificate ({@code String})
  • + *
  • Parameter 2: hex-encoded public key from signatures record ({@code String})
  • + *
+ */ + V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( + "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"), + + /** + * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures + * record do not match the signature algorithms listed in the signatures record. + * + *
    + *
  • Parameter 1: signature algorithms from signatures record ({@code List})
  • + *
  • Parameter 2: signature algorithms from digests record ({@code List})
  • + *
+ */ + V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS( + "Signature algorithms mismatch between signatures and digests records" + + ": %1$s vs %2$s"), + + /** + * The APK's digest does not match the digest contained in the APK Signature Scheme v2 + * signature. + * + *
    + *
  • Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
  • + *
  • Parameter 2: hex-encoded expected digest of the APK ({@code String})
  • + *
  • Parameter 3: hex-encoded actual digest of the APK ({@code String})
  • + *
+ */ + V2_SIG_APK_DIGEST_DID_NOT_VERIFY( + "APK integrity check failed. %1$s digest mismatch." + + " Expected: <%2$s>, actual: <%3$s>"), + + /** + * Failed to parse the list of signers contained in the APK Signature Scheme v3 signature. + */ + V3_SIG_MALFORMED_SIGNERS("Malformed list of signers"), + + /** + * Failed to parse this signer's signer block contained in the APK Signature Scheme v3 + * signature. + */ + V3_SIG_MALFORMED_SIGNER("Malformed signer block"), + + /** + * Public key embedded in the APK Signature Scheme v3 signature of this signer could not be + * parsed. + * + *
    + *
  • Parameter 1: error details ({@code Throwable})
  • + *
+ */ + V3_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), + + /** + * This APK Signature Scheme v3 signer's certificate could not be parsed. + * + *
    + *
  • Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of + * certificates ({@code Integer})
  • + *
  • Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's + * list of certificates ({@code Integer})
  • + *
  • Parameter 3: error details ({@code Throwable})
  • + *
+ */ + V3_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"), + + /** + * Failed to parse this signer's signature record contained in the APK Signature Scheme v3 + * signature. + * + *
    + *
  • Parameter 1: record number (first record is {@code 1}) ({@code Integer})
  • + *
+ */ + V3_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v3 signature record #%1$d"), + + /** + * Failed to parse this signer's digest record contained in the APK Signature Scheme v3 + * signature. + * + *
    + *
  • Parameter 1: record number (first record is {@code 1}) ({@code Integer})
  • + *
+ */ + V3_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v3 digest record #%1$d"), + + /** + * This APK Signature Scheme v3 signer contains a malformed additional attribute. + * + *
    + *
  • Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})
  • + *
+ */ + V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"), + + /** + * APK Signature Scheme v3 signature contains no signers. + */ + V3_SIG_NO_SIGNERS("No signers in APK Signature Scheme v3 signature"), + + /** + * APK Signature Scheme v3 signature contains multiple signers (only one allowed per + * platform version). + */ + V3_SIG_MULTIPLE_SIGNERS("Multiple APK Signature Scheme v3 signatures found for a single " + + " platform version."), + + /** + * APK Signature Scheme v3 signature found, but multiple v1 and/or multiple v2 signers + * found, where only one may be used with APK Signature Scheme v3 + */ + V3_SIG_MULTIPLE_PAST_SIGNERS("Multiple signatures found for pre-v3 signing with an APK " + + " Signature Scheme v3 signer. Only one allowed."), + + /** + * APK Signature Scheme v3 signature found, but its signer doesn't match the v1/v2 signers, + * or have them as the root of its signing certificate history + */ + V3_SIG_PAST_SIGNERS_MISMATCH( + "v3 signer differs from v1/v2 signer without proper signing certificate lineage."), + + /** + * This APK Signature Scheme v3 signer contains a signature produced using an unknown + * algorithm. + * + *
    + *
  • Parameter 1: algorithm ID ({@code Integer})
  • + *
+ */ + V3_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), + + /** + * This APK Signature Scheme v3 signer contains an unknown additional attribute. + * + *
    + *
  • Parameter 1: attribute ID ({@code Integer})
  • + *
+ */ + V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"), + + /** + * An exception was encountered while verifying APK Signature Scheme v3 signature of this + * signer. + * + *
    + *
  • Parameter 1: signature algorithm ({@link SignatureAlgorithm})
  • + *
  • Parameter 2: exception ({@code Throwable})
  • + *
+ */ + V3_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), + + /** + * The APK Signature Scheme v3 signer contained an invalid value for either min or max SDK + * versions. + * + *
    + *
  • Parameter 1: minSdkVersion ({@code Integer}) + *
  • Parameter 2: maxSdkVersion ({@code Integer}) + *
+ */ + V3_SIG_INVALID_SDK_VERSIONS("Invalid SDK Version parameter(s) encountered in APK Signature " + + "scheme v3 signature: minSdkVersion %1$s maxSdkVersion: %2$s"), + + /** + * APK Signature Scheme v3 signature over this signer's signed-data block did not verify. + * + *
    + *
  • Parameter 1: signature algorithm ({@link SignatureAlgorithm})
  • + *
+ */ + V3_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), + + /** + * This APK Signature Scheme v3 signer offers no signatures. + */ + V3_SIG_NO_SIGNATURES("No signatures"), + + /** + * This APK Signature Scheme v3 signer offers signatures but none of them are supported. + */ + V3_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"), + + /** + * This APK Signature Scheme v3 signer offers no certificates. + */ + V3_SIG_NO_CERTIFICATES("No certificates"), + + /** + * This APK Signature Scheme v3 signer's minSdkVersion listed in the signer's signed data + * does not match the minSdkVersion listed in the signatures record. + * + *
    + *
  • Parameter 1: minSdkVersion in signature record ({@code Integer})
  • + *
  • Parameter 2: minSdkVersion in signed data ({@code Integer})
  • + *
+ */ + V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD( + "minSdkVersion mismatch between signed data and signature record:" + + " <%1$s> vs <%2$s>"), + + /** + * This APK Signature Scheme v3 signer's maxSdkVersion listed in the signer's signed data + * does not match the maxSdkVersion listed in the signatures record. + * + *
    + *
  • Parameter 1: maxSdkVersion in signature record ({@code Integer})
  • + *
  • Parameter 2: maxSdkVersion in signed data ({@code Integer})
  • + *
+ */ + V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD( + "maxSdkVersion mismatch between signed data and signature record:" + + " <%1$s> vs <%2$s>"), + + /** + * This APK Signature Scheme v3 signer's public key listed in the signer's certificate does + * not match the public key listed in the signatures record. + * + *
    + *
  • Parameter 1: hex-encoded public key from certificate ({@code String})
  • + *
  • Parameter 2: hex-encoded public key from signatures record ({@code String})
  • + *
+ */ + V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( + "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"), + + /** + * This APK Signature Scheme v3 signer's signature algorithms listed in the signatures + * record do not match the signature algorithms listed in the signatures record. + * + *
    + *
  • Parameter 1: signature algorithms from signatures record ({@code List})
  • + *
  • Parameter 2: signature algorithms from digests record ({@code List})
  • + *
+ */ + V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS( + "Signature algorithms mismatch between signatures and digests records" + + ": %1$s vs %2$s"), + + /** + * The APK's digest does not match the digest contained in the APK Signature Scheme v3 + * signature. + * + *
    + *
  • Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
  • + *
  • Parameter 2: hex-encoded expected digest of the APK ({@code String})
  • + *
  • Parameter 3: hex-encoded actual digest of the APK ({@code String})
  • + *
+ */ + V3_SIG_APK_DIGEST_DID_NOT_VERIFY( + "APK integrity check failed. %1$s digest mismatch." + + " Expected: <%2$s>, actual: <%3$s>"), + + /** + * The signer's SigningCertificateLineage attribute containd a proof-of-rotation record with + * signature(s) that did not verify. + */ + V3_SIG_POR_DID_NOT_VERIFY("SigningCertificateLineage attribute containd a proof-of-rotation" + + " record with signature(s) that did not verify."), + + /** + * Failed to parse the SigningCertificateLineage structure in the APK Signature Scheme v3 + * signature's additional attributes section. + */ + V3_SIG_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage structure in the " + + "APK Signature Scheme v3 signature's additional attributes section."), + + /** + * The APK's signing certificate does not match the terminal node in the provided + * proof-of-rotation structure describing the signing certificate history + */ + V3_SIG_POR_CERT_MISMATCH( + "APK signing certificate differs from the associated certificate found in the " + + "signer's SigningCertificateLineage."), + + /** + * The APK Signature Scheme v3 signers encountered do not offer a continuous set of + * supported platform versions. Either they overlap, resulting in potentially two + * acceptable signers for a platform version, or there are holes which would create problems + * in the event of platform version upgrades. + */ + V3_INCONSISTENT_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK " + + "versions are not continuous."), + + /** + * The APK Signature Scheme v3 signers don't cover all requested SDK versions. + * + *
    + *
  • Parameter 1: minSdkVersion ({@code Integer}) + *
  • Parameter 2: maxSdkVersion ({@code Integer}) + *
+ */ + V3_MISSING_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK " + + "versions do not cover the entire desired range. Found min: %1$s max %2$s"), + + /** + * The SigningCertificateLineages for different platform versions using APK Signature Scheme + * v3 do not go together. Specifically, each should be a subset of another, with the size + * of each increasing as the platform level increases. + */ + V3_INCONSISTENT_LINEAGES("SigningCertificateLineages targeting different platform versions" + + " using APK Signature Scheme v3 are not all a part of the same overall lineage."), + + /** + * APK Signing Block contains an unknown entry. + * + *
    + *
  • Parameter 1: entry ID ({@code Integer})
  • + *
+ */ + APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"); + + private final String mFormat; + + private Issue(String format) { + mFormat = format; + } + + /** + * Returns the format string suitable for combining the parameters of this issue into a + * readable string. See {@link java.util.Formatter} for format. + */ + private String getFormat() { + return mFormat; + } + } + + /** + * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted + * form. + */ + public static class IssueWithParams { + private final Issue mIssue; + private final Object[] mParams; + + /** + * Constructs a new {@code IssueWithParams} of the specified type and with provided + * parameters. + */ + public IssueWithParams(Issue issue, Object[] params) { + mIssue = issue; + mParams = params; + } + + /** + * Returns the type of this issue. + */ + public Issue getIssue() { + return mIssue; + } + + /** + * Returns the parameters of this issue. + */ + public Object[] getParams() { + return mParams.clone(); + } + + /** + * Returns a readable form of this issue. + */ + @Override + public String toString() { + return String.format(mIssue.getFormat(), mParams); + } + } + + /** + * Wrapped around {@code byte[]} which ensures that {@code equals} and {@code hashCode} operate + * on the contents of the arrays rather than on references. + */ + private static class ByteArray { + private final byte[] mArray; + private final int mHashCode; + + private ByteArray(byte[] arr) { + mArray = arr; + mHashCode = Arrays.hashCode(mArray); + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ByteArray other = (ByteArray) obj; + if (hashCode() != other.hashCode()) { + return false; + } + if (!Arrays.equals(mArray, other.mArray)) { + return false; + } + return true; + } + } + + /** + * Builder of {@link ApkVerifier} instances. + * + *

The resulting verifier by default checks whether the APK will verify on all platform + * versions supported by the APK, as specified by {@code android:minSdkVersion} attributes in + * the APK's {@code AndroidManifest.xml}. The range of platform versions can be customized using + * {@link #setMinCheckedPlatformVersion(int)} and {@link #setMaxCheckedPlatformVersion(int)}. + */ + public static class Builder { + private final File mApkFile; + private final DataSource mApkDataSource; + + private Integer mMinSdkVersion; + private int mMaxSdkVersion = Integer.MAX_VALUE; + + /** + * Constructs a new {@code Builder} for verifying the provided APK file. + */ + public Builder(File apk) { + if (apk == null) { + throw new NullPointerException("apk == null"); + } + mApkFile = apk; + mApkDataSource = null; + } + + /** + * Constructs a new {@code Builder} for verifying the provided 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 is verified. APK verification + * will confirm that the APK is expected to install successfully on all known Android + * platforms starting from the platform version with the provided API Level. The upper end + * of the platform versions range can be modified via + * {@link #setMaxCheckedPlatformVersion(int)}. + * + *

This method is useful for overriding the default behavior which checks that the APK + * will verify on all platform versions supported by the APK, as specified by + * {@code android:minSdkVersion} attributes in the APK's {@code AndroidManifest.xml}. + * + * @param minSdkVersion API Level of the oldest platform for which to verify the APK + * + * @see #setMinCheckedPlatformVersion(int) + */ + public Builder setMinCheckedPlatformVersion(int minSdkVersion) { + mMinSdkVersion = minSdkVersion; + return this; + } + + /** + * Sets the newest Android platform version for which the APK is verified. APK verification + * will confirm that the APK is expected to install successfully on all platform versions + * supported by the APK up until and including the provided version. 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 Builder setMaxCheckedPlatformVersion(int maxSdkVersion) { + mMaxSdkVersion = maxSdkVersion; + return this; + } + + /** + * Returns an {@link ApkVerifier} initialized according to the configuration of this + * builder. + */ + public ApkVerifier build() { + return new ApkVerifier( + mApkFile, + mApkDataSource, + mMinSdkVersion, + mMaxSdkVersion); + } + } +} diff --git a/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java new file mode 100644 index 0000000..c88239e --- /dev/null +++ b/app/src/main/java/com/android/apksig/DefaultApkSignerEngine.java @@ -0,0 +1,1506 @@ +/* + * 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.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.v1.DigestAlgorithm; +import com.android.apksig.internal.apk.v1.V1SchemeSigner; +import com.android.apksig.internal.apk.v1.V1SchemeVerifier; +import com.android.apksig.internal.apk.v2.V2SchemeSigner; +import com.android.apksig.internal.apk.v3.V3SchemeSigner; +import com.android.apksig.internal.jar.ManifestParser; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.util.TeeDataSink; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSinks; +import com.android.apksig.util.DataSource; + +import com.android.apksig.util.RunnablesExecutor; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Default implementation of {@link ApkSignerEngine}. + * + *

Use {@link Builder} to obtain instances of this engine. + */ +public class DefaultApkSignerEngine implements ApkSignerEngine { + + // IMPLEMENTATION NOTE: This engine generates a signed APK as follows: + // 1. The engine asks its client to output input JAR entries which are not part of JAR + // signature. + // 2. If JAR signing (v1 signing) is enabled, the engine inspects the output JAR entries to + // compute their digests, to be placed into output META-INF/MANIFEST.MF. It also inspects + // the contents of input and output META-INF/MANIFEST.MF to borrow the main section of the + // file. It does not care about individual (i.e., JAR entry-specific) sections. It then + // emits the v1 signature (a set of JAR entries) and asks the client to output them. + // 3. If APK Signature Scheme v2 (v2 signing) is enabled, the engine emits an APK Signing Block + // from outputZipSections() and asks its client to insert this block into the output. + // 4. If APK Signature Scheme v3 (v3 signing) is enabled, the engine includes it in the APK + // Signing BLock output from outputZipSections() and asks its client to insert this block + // into the output. If both v2 and v3 signing is enabled, they are both added to the APK + // Signing Block before asking the client to insert it into the output. + + private final boolean mV1SigningEnabled; + private final boolean mV2SigningEnabled; + private final boolean mV3SigningEnabled; + private final boolean mDebuggableApkPermitted; + private final boolean mOtherSignersSignaturesPreserved; + private final String mCreatedBy; + private final List mSignerConfigs; + private final int mMinSdkVersion; + private final SigningCertificateLineage mSigningCertificateLineage; + + private List mV1SignerConfigs = Collections.emptyList(); + private DigestAlgorithm mV1ContentDigestAlgorithm; + + private boolean mClosed; + + private boolean mV1SignaturePending; + + /** + * Names of JAR entries which this engine is expected to output as part of v1 signing. + */ + private Set mSignatureExpectedOutputJarEntryNames = Collections.emptySet(); + + /** Requests for digests of output JAR entries. */ + private final Map mOutputJarEntryDigestRequests = + new HashMap<>(); + + /** Digests of output JAR entries. */ + private final Map mOutputJarEntryDigests = new HashMap<>(); + + /** Data of JAR entries emitted by this engine as v1 signature. */ + private final Map mEmittedSignatureJarEntryData = new HashMap<>(); + + /** Requests for data of output JAR entries which comprise the v1 signature. */ + private final Map mOutputSignatureJarEntryDataRequests = + new HashMap<>(); + /** + * Request to obtain the data of MANIFEST.MF or {@code null} if the request hasn't been issued. + */ + private GetJarEntryDataRequest mInputJarManifestEntryDataRequest; + + /** + * Request to obtain the data of AndroidManifest.xml or {@code null} if the request hasn't been + * issued. + */ + private GetJarEntryDataRequest mOutputAndroidManifestEntryDataRequest; + + /** + * Whether the package being signed is marked as {@code android:debuggable} or {@code null} + * if this is not yet known. + */ + private Boolean mDebuggable; + + /** + * Request to output the emitted v1 signature or {@code null} if the request hasn't been issued. + */ + private OutputJarSignatureRequestImpl mAddV1SignatureRequest; + + private boolean mV2SignaturePending; + private boolean mV3SignaturePending; + + /** + * Request to output the emitted v2 and/or v3 signature(s) {@code null} if the request hasn't + * been issued. + */ + private OutputApkSigningBlockRequestImpl mAddSigningBlockRequest; + + + private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED; + + private DefaultApkSignerEngine( + List signerConfigs, + int minSdkVersion, + boolean v1SigningEnabled, + boolean v2SigningEnabled, + boolean v3SigningEnabled, + boolean debuggableApkPermitted, + boolean otherSignersSignaturesPreserved, + String createdBy, + SigningCertificateLineage signingCertificateLineage) throws InvalidKeyException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + if (otherSignersSignaturesPreserved) { + throw new UnsupportedOperationException( + "Preserving other signer's signatures is not yet implemented"); + } + + mV1SigningEnabled = v1SigningEnabled; + mV2SigningEnabled = v2SigningEnabled; + mV3SigningEnabled = v3SigningEnabled; + mV1SignaturePending = v1SigningEnabled; + mV2SignaturePending = v2SigningEnabled; + mV3SignaturePending = v3SigningEnabled; + mDebuggableApkPermitted = debuggableApkPermitted; + mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved; + mCreatedBy = createdBy; + mSignerConfigs = signerConfigs; + mMinSdkVersion = minSdkVersion; + mSigningCertificateLineage = signingCertificateLineage; + + if (v1SigningEnabled) { + if (v3SigningEnabled) { + + // v3 signing only supports single signers, of which the oldest (first) will be the + // one to use for v1 and v2 signing + SignerConfig oldestConfig = signerConfigs.get(0); + + // in the event of signing certificate changes, make sure we have the oldest in the + // signing history to sign with v1 + if (signingCertificateLineage != null) { + SigningCertificateLineage subLineage = + signingCertificateLineage.getSubLineage( + oldestConfig.mCertificates.get(0)); + if (subLineage.size() != 1) { + throw new IllegalArgumentException( + "v1 signing enabled but the oldest signer in the " + + "SigningCertificateLineage is missing. Please provide the oldest" + + " signer to enable v1 signing"); + } + } + createV1SignerConfigs( + Collections.singletonList(oldestConfig), minSdkVersion); + } else { + createV1SignerConfigs(signerConfigs, minSdkVersion); + } + } + } + + private void createV1SignerConfigs(List signerConfigs, int minSdkVersion) + throws InvalidKeyException { + mV1SignerConfigs = new ArrayList<>(signerConfigs.size()); + Map v1SignerNameToSignerIndex = new HashMap<>(signerConfigs.size()); + DigestAlgorithm v1ContentDigestAlgorithm = null; + for (int i = 0; i < signerConfigs.size(); i++) { + SignerConfig signerConfig = signerConfigs.get(i); + List certificates = signerConfig.getCertificates(); + PublicKey publicKey = certificates.get(0).getPublicKey(); + + String v1SignerName = V1SchemeSigner.getSafeSignerName(signerConfig.getName()); + // Check whether the signer's name is unique among all v1 signers + Integer indexOfOtherSignerWithSameName = + v1SignerNameToSignerIndex.put(v1SignerName, i); + if (indexOfOtherSignerWithSameName != null) { + throw new IllegalArgumentException( + "Signers #" + (indexOfOtherSignerWithSameName + 1) + + " and #" + (i + 1) + + " have the same name: " + v1SignerName + + ". v1 signer names must be unique"); + } + + DigestAlgorithm v1SignatureDigestAlgorithm = + V1SchemeSigner.getSuggestedSignatureDigestAlgorithm( + publicKey, minSdkVersion); + V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig(); + v1SignerConfig.name = v1SignerName; + v1SignerConfig.privateKey = signerConfig.getPrivateKey(); + v1SignerConfig.certificates = certificates; + v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm; + // For digesting contents of APK entries and of MANIFEST.MF, pick the algorithm + // of comparable strength to the digest algorithm used for computing the signature. + // When there are multiple signers, pick the strongest digest algorithm out of their + // signature digest algorithms. This avoids reducing the digest strength used by any + // of the signers to protect APK contents. + if (v1ContentDigestAlgorithm == null) { + v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm; + } else { + if (DigestAlgorithm.BY_STRENGTH_COMPARATOR.compare( + v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm) > 0) { + v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm; + } + } + mV1SignerConfigs.add(v1SignerConfig); + } + mV1ContentDigestAlgorithm = v1ContentDigestAlgorithm; + mSignatureExpectedOutputJarEntryNames = + V1SchemeSigner.getOutputEntryNames(mV1SignerConfigs); + } + + private List createV2SignerConfigs( + boolean apkSigningBlockPaddingSupported) throws InvalidKeyException { + if (mV3SigningEnabled) { + + // v3 signing only supports single signers, of which the oldest (first) will be the one + // to use for v1 and v2 signing + List signerConfig = + new ArrayList<>(); + + SignerConfig oldestConfig = mSignerConfigs.get(0); + + // first make sure that if we have signing certificate history that the oldest signer + // corresponds to the oldest ancestor + if (mSigningCertificateLineage != null) { + SigningCertificateLineage subLineage = + mSigningCertificateLineage.getSubLineage(oldestConfig.mCertificates.get(0)); + if (subLineage.size() != 1) { + throw new IllegalArgumentException("v2 signing enabled but the oldest signer in" + + " the SigningCertificateLineage is missing. Please provide" + + " the oldest signer to enable v2 signing."); + } + } + signerConfig.add( + createSigningBlockSignerConfig( + mSignerConfigs.get(0), apkSigningBlockPaddingSupported, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2)); + return signerConfig; + } else { + return createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); + } + } + + private List createV3SignerConfigs( + boolean apkSigningBlockPaddingSupported) throws InvalidKeyException { + List rawConfigs = + createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + + List processedConfigs = new ArrayList<>(); + + // we have our configs, now touch them up to appropriately cover all SDK levels since APK + // signature scheme v3 was introduced + int currentMinSdk = Integer.MAX_VALUE; + for (int i = rawConfigs.size() - 1; i >= 0; i--) { + ApkSigningBlockUtils.SignerConfig config = rawConfigs.get(i); + if (config.signatureAlgorithms == null) { + // no valid algorithm was found for this signer, and we haven't yet covered all + // platform versions, something's wrong + String keyAlgorithm = config.certificates.get(0).getPublicKey().getAlgorithm(); + throw new InvalidKeyException("Unsupported key algorithm " + keyAlgorithm + " is " + + "not supported for APK Signature Scheme v3 signing"); + } + if (i == rawConfigs.size() - 1) { + // first go through the loop, config should support all future platform versions. + // this assumes we don't deprecate support for signers in the future. If we do, + // this needs to change + config.maxSdkVersion = Integer.MAX_VALUE; + } else { + // otherwise, we only want to use this signer up to the minimum platform version + // on which a newer one is acceptable + config.maxSdkVersion = currentMinSdk - 1; + } + config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(config.signatureAlgorithms); + if (mSigningCertificateLineage != null) { + config.mSigningCertificateLineage = + mSigningCertificateLineage.getSubLineage(config.certificates.get(0)); + } + // we know that this config will be used, so add it to our result, order doesn't matter + // at this point (and likely only one will be needed + processedConfigs.add(config); + currentMinSdk = config.minSdkVersion; + if (currentMinSdk <= mMinSdkVersion || currentMinSdk <= AndroidSdkVersion.P) { + // this satisfies all we need, stop here + break; + } + } + if (currentMinSdk > AndroidSdkVersion.P && currentMinSdk > mMinSdkVersion) { + // we can't cover all desired SDK versions, abort + throw new InvalidKeyException("Provided key algorithms not supported on all desired " + + "Android SDK versions"); + } + return processedConfigs; + } + + private int getMinSdkFromV3SignatureAlgorithms(List algorithms) { + int min = Integer.MAX_VALUE; + for (SignatureAlgorithm algorithm : algorithms) { + int current = algorithm.getMinSdkVersion(); + if (current < min) { + if (current <= mMinSdkVersion || current <= AndroidSdkVersion.P) { + // this algorithm satisfies all of our needs, no need to keep looking + return current; + } else { + min = current; + } + } + } + return min; + } + + private List createSigningBlockSignerConfigs( + boolean apkSigningBlockPaddingSupported, int schemeId) throws InvalidKeyException { + List signerConfigs = + new ArrayList<>(mSignerConfigs.size()); + for (int i = 0; i < mSignerConfigs.size(); i++) { + SignerConfig signerConfig = mSignerConfigs.get(i); + signerConfigs.add( + createSigningBlockSignerConfig( + signerConfig, apkSigningBlockPaddingSupported, schemeId)); + } + return signerConfigs; + } + + private ApkSigningBlockUtils.SignerConfig createSigningBlockSignerConfig( + SignerConfig signerConfig, boolean apkSigningBlockPaddingSupported, int schemeId) + throws InvalidKeyException { + List certificates = signerConfig.getCertificates(); + PublicKey publicKey = certificates.get(0).getPublicKey(); + + ApkSigningBlockUtils.SignerConfig newSignerConfig = + new ApkSigningBlockUtils.SignerConfig(); + newSignerConfig.privateKey = signerConfig.getPrivateKey(); + newSignerConfig.certificates = certificates; + + switch (schemeId) { + case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2: + newSignerConfig.signatureAlgorithms = + V2SchemeSigner.getSuggestedSignatureAlgorithms(publicKey, mMinSdkVersion, + apkSigningBlockPaddingSupported); + break; + case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3: + try { + newSignerConfig.signatureAlgorithms = + V3SchemeSigner.getSuggestedSignatureAlgorithms( + publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported); + } catch (InvalidKeyException e) { + + // It is possible for a signer used for v1/v2 signing to not be allowed for use + // with v3 signing. This is ok as long as there exists a more recent v3 signer + // that covers all supported platform versions. Populate signatureAlgorithm + // with null, it will be cleaned-up in a later step. + newSignerConfig.signatureAlgorithms = null; + } + break; + default: + throw new IllegalArgumentException("Unknown APK Signature Scheme ID requested"); + } + return newSignerConfig; + } + + private boolean isDebuggable(String entryName) { + return mDebuggableApkPermitted + || !ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(entryName); + } + + /** + * Initializes DefaultApkSignerEngine with the existing MANIFEST.MF. This reads existing digests + * from the MANIFEST.MF file (they are assumed correct) and stores them for the final signature + * without recalculation. This step has a significant performance benefit in case of incremental + * build. + * + * This method extracts and stored computed digest for every entry that it would compute it for + * in the {@link #outputJarEntry(String)} method + * + * @param manifestBytes raw representation of MANIFEST.MF file + * @param entryNames a set of expected entries names + * @return set of entry names which were processed by the engine during the initialization, a + * subset of entryNames + */ + @Override + @SuppressWarnings("AndroidJdkLibsChecker") + public Set initWith(byte[] manifestBytes, Set entryNames) { + V1SchemeVerifier.Result dummyResult = new V1SchemeVerifier.Result(); + Pair> sections = + V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult); + String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm); + for (Map.Entry entry: sections.getSecond().entrySet()) { + String entryName = entry.getKey(); + if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) && + isDebuggable(entryName)) { + + Optional extractedDigest = + V1SchemeVerifier.getDigestsToVerify( + entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE) + .stream() + .filter(d -> d.jcaDigestAlgorithm == alg) + .findFirst(); + + extractedDigest.ifPresent( + namedDigest -> mOutputJarEntryDigests.put(entryName, namedDigest.digest)); + } + } + return mOutputJarEntryDigests.keySet(); + } + + @Override + public void setExecutor(RunnablesExecutor executor) { + mExecutor = executor; + } + + @Override + public void inputApkSigningBlock(DataSource apkSigningBlock) { + checkNotClosed(); + + if ((apkSigningBlock == null) || (apkSigningBlock.size() == 0)) { + return; + } + + if (mOtherSignersSignaturesPreserved) { + // TODO: Preserve blocks other than APK Signature Scheme v2 blocks of signers configured + // in this engine. + return; + } + // TODO: Preserve blocks other than APK Signature Scheme v2 blocks. + } + + @Override + public InputJarEntryInstructions inputJarEntry(String entryName) { + checkNotClosed(); + + InputJarEntryInstructions.OutputPolicy outputPolicy = + getInputJarEntryOutputPolicy(entryName); + switch (outputPolicy) { + case SKIP: + return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.SKIP); + case OUTPUT: + return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT); + case OUTPUT_BY_ENGINE: + if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) { + // We copy the main section of the JAR manifest from input to output. Thus, this + // invalidates v1 signature and we need to see the entry's data. + mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName); + return new InputJarEntryInstructions( + InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE, + mInputJarManifestEntryDataRequest); + } + return new InputJarEntryInstructions( + InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE); + default: + throw new RuntimeException("Unsupported output policy: " + outputPolicy); + } + } + + @Override + public InspectJarEntryRequest outputJarEntry(String entryName) { + checkNotClosed(); + invalidateV2Signature(); + + if (!isDebuggable(entryName)) { + forgetOutputApkDebuggableStatus(); + } + + if (!mV1SigningEnabled) { + // No need to inspect JAR entries when v1 signing is not enabled. + if (!isDebuggable(entryName)) { + // To reject debuggable APKs we need to inspect the APK's AndroidManifest.xml to + // check whether it declares that the APK is debuggable + mOutputAndroidManifestEntryDataRequest = new GetJarEntryDataRequest(entryName); + return mOutputAndroidManifestEntryDataRequest; + } + return null; + } + // v1 signing is enabled + + if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) { + // This entry is covered by v1 signature. We thus need to inspect the entry's data to + // compute its digest(s) for v1 signature. + + // TODO: Handle the case where other signer's v1 signatures are present and need to be + // preserved. In that scenario we can't modify MANIFEST.MF and add/remove JAR entries + // covered by v1 signature. + invalidateV1Signature(); + GetJarEntryDataDigestRequest dataDigestRequest = + new GetJarEntryDataDigestRequest( + entryName, + V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm)); + mOutputJarEntryDigestRequests.put(entryName, dataDigestRequest); + mOutputJarEntryDigests.remove(entryName); + + if ((!mDebuggableApkPermitted) + && (ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(entryName))) { + // To reject debuggable APKs we need to inspect the APK's AndroidManifest.xml to + // check whether it declares that the APK is debuggable + mOutputAndroidManifestEntryDataRequest = new GetJarEntryDataRequest(entryName); + return new CompoundInspectJarEntryRequest( + entryName, mOutputAndroidManifestEntryDataRequest, dataDigestRequest); + } + + return dataDigestRequest; + } + + if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) { + // This entry is part of v1 signature generated by this engine. We need to check whether + // the entry's data is as output by the engine. + invalidateV1Signature(); + GetJarEntryDataRequest dataRequest; + if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) { + dataRequest = new GetJarEntryDataRequest(entryName); + mInputJarManifestEntryDataRequest = dataRequest; + } else { + // If this entry is part of v1 signature which has been emitted by this engine, + // check whether the output entry's data matches what the engine emitted. + dataRequest = + (mEmittedSignatureJarEntryData.containsKey(entryName)) + ? new GetJarEntryDataRequest(entryName) : null; + } + + if (dataRequest != null) { + mOutputSignatureJarEntryDataRequests.put(entryName, dataRequest); + } + return dataRequest; + } + + // This entry is not covered by v1 signature and isn't part of v1 signature. + return null; + } + + @Override + public InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) { + checkNotClosed(); + return getInputJarEntryOutputPolicy(entryName); + } + + @Override + public void outputJarEntryRemoved(String entryName) { + checkNotClosed(); + invalidateV2Signature(); + if (!mV1SigningEnabled) { + return; + } + + if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) { + // This entry is covered by v1 signature. + invalidateV1Signature(); + mOutputJarEntryDigests.remove(entryName); + mOutputJarEntryDigestRequests.remove(entryName); + mOutputSignatureJarEntryDataRequests.remove(entryName); + return; + } + + if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) { + // This entry is part of the v1 signature generated by this engine. + invalidateV1Signature(); + return; + } + } + + @Override + public OutputJarSignatureRequest outputJarEntries() + throws ApkFormatException, InvalidKeyException, SignatureException, + NoSuchAlgorithmException { + checkNotClosed(); + + if (!mV1SignaturePending) { + return null; + } + + if ((mInputJarManifestEntryDataRequest != null) + && (!mInputJarManifestEntryDataRequest.isDone())) { + throw new IllegalStateException( + "Still waiting to inspect input APK's " + + mInputJarManifestEntryDataRequest.getEntryName()); + } + + for (GetJarEntryDataDigestRequest digestRequest + : mOutputJarEntryDigestRequests.values()) { + String entryName = digestRequest.getEntryName(); + if (!digestRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + entryName); + } + mOutputJarEntryDigests.put(entryName, digestRequest.getDigest()); + } + mOutputJarEntryDigestRequests.clear(); + + for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) { + if (!dataRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + dataRequest.getEntryName()); + } + } + + List apkSigningSchemeIds = new ArrayList<>(); + if (mV2SigningEnabled) { + apkSigningSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); + } + if (mV3SigningEnabled) { + apkSigningSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + } + byte[] inputJarManifest = + (mInputJarManifestEntryDataRequest != null) + ? mInputJarManifestEntryDataRequest.getData() : null; + + // Check whether the most recently used signature (if present) is still fine. + checkOutputApkNotDebuggableIfDebuggableMustBeRejected(); + List> signatureZipEntries; + if ((mAddV1SignatureRequest == null) || (!mAddV1SignatureRequest.isDone())) { + try { + signatureZipEntries = + V1SchemeSigner.sign( + mV1SignerConfigs, + mV1ContentDigestAlgorithm, + mOutputJarEntryDigests, + apkSigningSchemeIds, + inputJarManifest, + mCreatedBy); + } catch (CertificateException e) { + throw new SignatureException("Failed to generate v1 signature", e); + } + } else { + V1SchemeSigner.OutputManifestFile newManifest = + V1SchemeSigner.generateManifestFile( + mV1ContentDigestAlgorithm, + mOutputJarEntryDigests, + inputJarManifest); + byte[] emittedSignatureManifest = + mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME); + if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) { + // Emitted v1 signature is no longer valid. + try { + signatureZipEntries = + V1SchemeSigner.signManifest( + mV1SignerConfigs, + mV1ContentDigestAlgorithm, + apkSigningSchemeIds, + mCreatedBy, + newManifest); + } catch (CertificateException e) { + throw new SignatureException("Failed to generate v1 signature", e); + } + } else { + // Emitted v1 signature is still valid. Check whether the signature is there in the + // output. + signatureZipEntries = new ArrayList<>(); + for (Map.Entry expectedOutputEntry + : mEmittedSignatureJarEntryData.entrySet()) { + String entryName = expectedOutputEntry.getKey(); + byte[] expectedData = expectedOutputEntry.getValue(); + GetJarEntryDataRequest actualDataRequest = + mOutputSignatureJarEntryDataRequests.get(entryName); + if (actualDataRequest == null) { + // This signature entry hasn't been output. + signatureZipEntries.add(Pair.of(entryName, expectedData)); + continue; + } + byte[] actualData = actualDataRequest.getData(); + if (!Arrays.equals(expectedData, actualData)) { + signatureZipEntries.add(Pair.of(entryName, expectedData)); + } + } + if (signatureZipEntries.isEmpty()) { + // v1 signature in the output is valid + return null; + } + // v1 signature in the output is not valid. + } + } + + if (signatureZipEntries.isEmpty()) { + // v1 signature in the output is valid + mV1SignaturePending = false; + return null; + } + + List sigEntries = + new ArrayList<>(signatureZipEntries.size()); + for (Pair entry : signatureZipEntries) { + String entryName = entry.getFirst(); + byte[] entryData = entry.getSecond(); + sigEntries.add(new OutputJarSignatureRequest.JarEntry(entryName, entryData)); + mEmittedSignatureJarEntryData.put(entryName, entryData); + } + mAddV1SignatureRequest = new OutputJarSignatureRequestImpl(sigEntries); + return mAddV1SignatureRequest; + } + + @Deprecated + @Override + public OutputApkSigningBlockRequest outputZipSections( + DataSource zipEntries, + DataSource zipCentralDirectory, + DataSource zipEocd) + throws IOException, InvalidKeyException, SignatureException, + NoSuchAlgorithmException { + return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, false); + } + + @Override + public OutputApkSigningBlockRequest2 outputZipSections2( + DataSource zipEntries, + DataSource zipCentralDirectory, + DataSource zipEocd) + throws IOException, InvalidKeyException, SignatureException, + NoSuchAlgorithmException { + return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, true); + } + + private OutputApkSigningBlockRequestImpl outputZipSectionsInternal( + DataSource zipEntries, + DataSource zipCentralDirectory, + DataSource zipEocd, + boolean apkSigningBlockPaddingSupported) + throws IOException, InvalidKeyException, SignatureException, + NoSuchAlgorithmException { + checkNotClosed(); + checkV1SigningDoneIfEnabled(); + if (!mV2SigningEnabled && !mV3SigningEnabled) { + return null; + } + checkOutputApkNotDebuggableIfDebuggableMustBeRejected(); + + // adjust to proper padding + Pair paddingPair = + ApkSigningBlockUtils.generateApkSigningBlockPadding(zipEntries, + apkSigningBlockPaddingSupported); + DataSource beforeCentralDir = paddingPair.getFirst(); + int padSizeBeforeApkSigningBlock = paddingPair.getSecond(); + DataSource eocd = + ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd); + + List> signingSchemeBlocks = new ArrayList<>(); + + // create APK Signature Scheme V2 Signature if requested + if (mV2SigningEnabled) { + invalidateV2Signature(); + List v2SignerConfigs = + createV2SignerConfigs(apkSigningBlockPaddingSupported); + signingSchemeBlocks.add( + V2SchemeSigner.generateApkSignatureSchemeV2Block( + mExecutor, + beforeCentralDir, + zipCentralDirectory, + eocd, + v2SignerConfigs, + mV3SigningEnabled)); + } + if (mV3SigningEnabled) { + invalidateV3Signature(); + List v3SignerConfigs = + createV3SignerConfigs(apkSigningBlockPaddingSupported); + signingSchemeBlocks.add( + V3SchemeSigner.generateApkSignatureSchemeV3Block( + mExecutor, + beforeCentralDir, + zipCentralDirectory, + eocd, + v3SignerConfigs)); + } + + // create APK Signing Block with v2 and/or v3 blocks + byte[] apkSigningBlock = + ApkSigningBlockUtils.generateApkSigningBlock(signingSchemeBlocks); + + mAddSigningBlockRequest = new OutputApkSigningBlockRequestImpl(apkSigningBlock, + padSizeBeforeApkSigningBlock); + return mAddSigningBlockRequest; + } + + @Override + public void outputDone() { + checkNotClosed(); + checkV1SigningDoneIfEnabled(); + checkSigningBlockDoneIfEnabled(); + } + + @Override + public void close() { + mClosed = true; + + mAddV1SignatureRequest = null; + mInputJarManifestEntryDataRequest = null; + mOutputAndroidManifestEntryDataRequest = null; + mDebuggable = null; + mOutputJarEntryDigestRequests.clear(); + mOutputJarEntryDigests.clear(); + mEmittedSignatureJarEntryData.clear(); + mOutputSignatureJarEntryDataRequests.clear(); + + mAddSigningBlockRequest = null; + } + + private void invalidateV1Signature() { + if (mV1SigningEnabled) { + mV1SignaturePending = true; + } + invalidateV2Signature(); + } + + private void invalidateV2Signature() { + if (mV2SigningEnabled) { + mV2SignaturePending = true; + mAddSigningBlockRequest = null; + } + } + + private void invalidateV3Signature() { + if (mV3SigningEnabled) { + mV3SignaturePending = true; + mAddSigningBlockRequest = null; + } + } + + private void checkNotClosed() { + if (mClosed) { + throw new IllegalStateException("Engine closed"); + } + } + + private void checkV1SigningDoneIfEnabled() { + if (!mV1SignaturePending) { + return; + } + + if (mAddV1SignatureRequest == null) { + throw new IllegalStateException( + "v1 signature (JAR signature) not yet generated. Skipped outputJarEntries()?"); + } + if (!mAddV1SignatureRequest.isDone()) { + throw new IllegalStateException( + "v1 signature (JAR signature) addition requested by outputJarEntries() hasn't" + + " been fulfilled"); + } + for (Map.Entry expectedOutputEntry + : mEmittedSignatureJarEntryData.entrySet()) { + String entryName = expectedOutputEntry.getKey(); + byte[] expectedData = expectedOutputEntry.getValue(); + GetJarEntryDataRequest actualDataRequest = + mOutputSignatureJarEntryDataRequests.get(entryName); + if (actualDataRequest == null) { + throw new IllegalStateException( + "APK entry " + entryName + " not yet output despite this having been" + + " requested"); + } else if (!actualDataRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + entryName); + } + byte[] actualData = actualDataRequest.getData(); + if (!Arrays.equals(expectedData, actualData)) { + throw new IllegalStateException( + "Output APK entry " + entryName + " data differs from what was requested"); + } + } + mV1SignaturePending = false; + } + + private void checkSigningBlockDoneIfEnabled() { + if (!mV2SignaturePending && !mV3SignaturePending) { + return; + } + if (mAddSigningBlockRequest == null) { + throw new IllegalStateException( + "Signed APK Signing BLock not yet generated. Skipped outputZipSections()?"); + } + if (!mAddSigningBlockRequest.isDone()) { + throw new IllegalStateException( + "APK Signing Block addition of signature(s) requested by" + + " outputZipSections() hasn't been fulfilled yet"); + } + mAddSigningBlockRequest = null; + mV2SignaturePending = false; + mV3SignaturePending = false; + } + + private void checkOutputApkNotDebuggableIfDebuggableMustBeRejected() + throws SignatureException { + if (mDebuggableApkPermitted) { + return; + } + + try { + if (isOutputApkDebuggable()) { + throw new SignatureException( + "APK is debuggable (see android:debuggable attribute) and this engine is" + + " configured to refuse to sign debuggable APKs"); + } + } catch (ApkFormatException e) { + throw new SignatureException("Failed to determine whether the APK is debuggable", e); + } + } + + /** + * Returns whether the output APK is debuggable according to its + * {@code android:debuggable} declaration. + */ + private boolean isOutputApkDebuggable() throws ApkFormatException { + if (mDebuggable != null) { + return mDebuggable; + } + + if (mOutputAndroidManifestEntryDataRequest == null) { + throw new IllegalStateException( + "Cannot determine debuggable status of output APK because " + + ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME + + " entry contents have not yet been requested"); + } + + if (!mOutputAndroidManifestEntryDataRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + + mOutputAndroidManifestEntryDataRequest.getEntryName()); + } + mDebuggable = + ApkUtils.getDebuggableFromBinaryAndroidManifest( + ByteBuffer.wrap(mOutputAndroidManifestEntryDataRequest.getData())); + return mDebuggable; + } + + private void forgetOutputApkDebuggableStatus() { + mDebuggable = null; + } + + /** + * Returns the output policy for the provided input JAR entry. + */ + private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) { + if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) { + return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE; + } + if ((mOtherSignersSignaturesPreserved) + || (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName))) { + return InputJarEntryInstructions.OutputPolicy.OUTPUT; + } + return InputJarEntryInstructions.OutputPolicy.SKIP; + } + + private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest { + private final List mAdditionalJarEntries; + private volatile boolean mDone; + + private OutputJarSignatureRequestImpl(List additionalZipEntries) { + mAdditionalJarEntries = + Collections.unmodifiableList(new ArrayList<>(additionalZipEntries)); + } + + @Override + public List getAdditionalJarEntries() { + return mAdditionalJarEntries; + } + + @Override + public void done() { + mDone = true; + } + + private boolean isDone() { + return mDone; + } + } + + @SuppressWarnings("deprecation") + private static class OutputApkSigningBlockRequestImpl + implements OutputApkSigningBlockRequest, OutputApkSigningBlockRequest2 { + private final byte[] mApkSigningBlock; + private final int mPaddingBeforeApkSigningBlock; + private volatile boolean mDone; + + private OutputApkSigningBlockRequestImpl(byte[] apkSigingBlock, int paddingBefore) { + mApkSigningBlock = apkSigingBlock.clone(); + mPaddingBeforeApkSigningBlock = paddingBefore; + } + + @Override + public byte[] getApkSigningBlock() { + return mApkSigningBlock.clone(); + } + + @Override + public void done() { + mDone = true; + } + + private boolean isDone() { + return mDone; + } + + @Override + public int getPaddingSizeBeforeApkSigningBlock() { + return mPaddingBeforeApkSigningBlock; + } + } + + /** + * JAR entry inspection request which obtain the entry's uncompressed data. + */ + private static class GetJarEntryDataRequest implements InspectJarEntryRequest { + private final String mEntryName; + private final Object mLock = new Object(); + + private boolean mDone; + private DataSink mDataSink; + private ByteArrayOutputStream mDataSinkBuf; + + private GetJarEntryDataRequest(String entryName) { + mEntryName = entryName; + } + + @Override + public String getEntryName() { + return mEntryName; + } + + @Override + public DataSink getDataSink() { + synchronized (mLock) { + checkNotDone(); + if (mDataSinkBuf == null) { + mDataSinkBuf = new ByteArrayOutputStream(); + } + if (mDataSink == null) { + mDataSink = DataSinks.asDataSink(mDataSinkBuf); + } + return mDataSink; + } + } + + @Override + public void done() { + synchronized (mLock) { + if (mDone) { + return; + } + mDone = true; + } + } + + private boolean isDone() { + synchronized (mLock) { + return mDone; + } + } + + private void checkNotDone() throws IllegalStateException { + synchronized (mLock) { + if (mDone) { + throw new IllegalStateException("Already done"); + } + } + } + + private byte[] getData() { + synchronized (mLock) { + if (!mDone) { + throw new IllegalStateException("Not yet done"); + } + return (mDataSinkBuf != null) ? mDataSinkBuf.toByteArray() : new byte[0]; + } + } + } + + /** + * JAR entry inspection request which obtains the digest of the entry's uncompressed data. + */ + private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest { + private final String mEntryName; + private final String mJcaDigestAlgorithm; + private final Object mLock = new Object(); + + private boolean mDone; + private DataSink mDataSink; + private MessageDigest mMessageDigest; + private byte[] mDigest; + + private GetJarEntryDataDigestRequest(String entryName, String jcaDigestAlgorithm) { + mEntryName = entryName; + mJcaDigestAlgorithm = jcaDigestAlgorithm; + } + + @Override + public String getEntryName() { + return mEntryName; + } + + @Override + public DataSink getDataSink() { + synchronized (mLock) { + checkNotDone(); + if (mDataSink == null) { + mDataSink = DataSinks.asDataSink(getMessageDigest()); + } + return mDataSink; + } + } + + private MessageDigest getMessageDigest() { + synchronized (mLock) { + if (mMessageDigest == null) { + try { + mMessageDigest = MessageDigest.getInstance(mJcaDigestAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException( + mJcaDigestAlgorithm + " MessageDigest not available", e); + } + } + return mMessageDigest; + } + } + + @Override + public void done() { + synchronized (mLock) { + if (mDone) { + return; + } + mDone = true; + mDigest = getMessageDigest().digest(); + mMessageDigest = null; + mDataSink = null; + } + } + + private boolean isDone() { + synchronized (mLock) { + return mDone; + } + } + + private void checkNotDone() throws IllegalStateException { + synchronized (mLock) { + if (mDone) { + throw new IllegalStateException("Already done"); + } + } + } + + private byte[] getDigest() { + synchronized (mLock) { + if (!mDone) { + throw new IllegalStateException("Not yet done"); + } + return mDigest.clone(); + } + } + } + + /** + * JAR entry inspection request which transparently satisfies multiple such requests. + */ + private static class CompoundInspectJarEntryRequest implements InspectJarEntryRequest { + private final String mEntryName; + private final InspectJarEntryRequest[] mRequests; + private final Object mLock = new Object(); + + private DataSink mSink; + + private CompoundInspectJarEntryRequest( + String entryName, InspectJarEntryRequest... requests) { + mEntryName = entryName; + mRequests = requests; + } + + @Override + public String getEntryName() { + return mEntryName; + } + + @Override + public DataSink getDataSink() { + synchronized (mLock) { + if (mSink == null) { + DataSink[] sinks = new DataSink[mRequests.length]; + for (int i = 0; i < sinks.length; i++) { + sinks[i] = mRequests[i].getDataSink(); + } + mSink = new TeeDataSink(sinks); + } + return mSink; + } + } + + @Override + public void done() { + for (InspectJarEntryRequest request : mRequests) { + request.done(); + } + } + } + + /** + * Configuration of a signer. + * + *

Use {@link Builder} to obtain configuration instances. + */ + public static class SignerConfig { + private final String mName; + private final PrivateKey mPrivateKey; + private final List mCertificates; + + private SignerConfig( + String name, + PrivateKey privateKey, + List certificates) { + mName = name; + mPrivateKey = privateKey; + mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates)); + } + + /** + * Returns the name of this signer. + */ + public String getName() { + return mName; + } + + /** + * Returns the signing key of this signer. + */ + public PrivateKey getPrivateKey() { + return mPrivateKey; + } + + /** + * Returns the certificate(s) of this signer. The first certificate's public key corresponds + * to this signer's private key. + */ + public List getCertificates() { + return mCertificates; + } + + /** + * Builder of {@link SignerConfig} instances. + */ + public static class Builder { + private final String mName; + private final PrivateKey mPrivateKey; + private final List mCertificates; + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + */ + public Builder( + String name, + PrivateKey privateKey, + List certificates) { + if (name.isEmpty()) { + throw new IllegalArgumentException("Empty name"); + } + mName = name; + mPrivateKey = privateKey; + mCertificates = new ArrayList<>(certificates); + } + + /** + * Returns a new {@code SignerConfig} instance configured based on the configuration of + * this builder. + */ + public SignerConfig build() { + return new SignerConfig( + mName, + mPrivateKey, + mCertificates); + } + } + } + + /** + * Builder of {@link DefaultApkSignerEngine} instances. + */ + public static class Builder { + private List mSignerConfigs; + private final int mMinSdkVersion; + + private boolean mV1SigningEnabled = true; + private boolean mV2SigningEnabled = true; + private boolean mV3SigningEnabled = true; + private boolean mDebuggableApkPermitted = true; + private boolean mOtherSignersSignaturesPreserved; + private String mCreatedBy = "1.0 (Android)"; + + private SigningCertificateLineage mSigningCertificateLineage; + + // APK Signature Scheme v3 only supports a single signing certificate, so to move to v3 + // signing by default, but not require prior clients to update to explicitly disable v3 + // signing for multiple signers, we modify the mV3SigningEnabled depending on the provided + // inputs (multiple signers and mSigningCertificateLineage in particular). Maintain two + // extra variables to record whether or not mV3SigningEnabled has been set directly by a + // client and so should override the default behavior. + private boolean mV3SigningExplicitlyDisabled = false; + private boolean mV3SigningExplicitlyEnabled = false; + + /** + * Constructs a new {@code Builder}. + * + * @param signerConfigs information about signers with which the APK will be signed. At + * least one signer configuration must be provided. + * @param minSdkVersion API Level of the oldest Android platform on which the APK is + * supposed to be installed. See {@code minSdkVersion} attribute in the APK's + * {@code AndroidManifest.xml}. The higher the version, the stronger signing features + * will be enabled. + */ + public Builder( + List signerConfigs, + int minSdkVersion) { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + if (signerConfigs.size() > 1) { + // APK Signature Scheme v3 only supports single signer, unless a + // SigningCertificateLineage is provided, in which case this will be reset to true, + // since we don't yet have a v4 scheme about which to worry + mV3SigningEnabled = false; + } + mSignerConfigs = new ArrayList<>(signerConfigs); + mMinSdkVersion = minSdkVersion; + } + + /** + * Returns a new {@code DefaultApkSignerEngine} instance configured based on the + * configuration of this builder. + */ + public DefaultApkSignerEngine build() throws InvalidKeyException { + + if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) { + throw new IllegalStateException("Builder configured to both enable and disable APK " + + "Signature Scheme v3 signing"); + } + if (mV3SigningExplicitlyDisabled) { + mV3SigningEnabled = false; + } else if (mV3SigningExplicitlyEnabled) { + mV3SigningEnabled = true; + } + + // make sure our signers are appropriately setup + if (mSigningCertificateLineage != null) { + try { + mSignerConfigs = mSigningCertificateLineage.sortSignerConfigs(mSignerConfigs); + if (!mV3SigningEnabled && mSignerConfigs.size() > 1) { + + // this is a strange situation: we've provided a valid rotation history, but + // are only signing with v1/v2. blow up, since we don't know for sure with + // which signer the user intended to sign + throw new IllegalStateException("Provided multiple signers which are part " + + "of the SigningCertificateLineage, but not signing with APK " + + "Signature Scheme v3"); + } + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Provided signer configs do not match the " + + "provided SigningCertificateLineage", e); + } + } else if (mV3SigningEnabled && mSignerConfigs.size() > 1) { + throw new IllegalStateException("Multiple signing certificates provided for use " + + "with APK Signature Scheme v3 without an accompanying SigningCertificateLineage"); + } + + return new DefaultApkSignerEngine( + mSignerConfigs, + mMinSdkVersion, + mV1SigningEnabled, + mV2SigningEnabled, + mV3SigningEnabled, + mDebuggableApkPermitted, + mOtherSignersSignaturesPreserved, + mCreatedBy, + mSigningCertificateLineage); + } + + /** + * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme). + * + *

By default, the APK will be signed using this scheme. + */ + public Builder setV1SigningEnabled(boolean enabled) { + mV1SigningEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature + * scheme). + * + *

By default, the APK will be signed using this scheme. + */ + public Builder setV2SigningEnabled(boolean enabled) { + mV2SigningEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed using APK Signature Scheme v3 (aka v3 signature + * scheme). + * + *

By default, the APK will be signed using this scheme. + */ + public Builder setV3SigningEnabled(boolean enabled) { + mV3SigningEnabled = enabled; + if (enabled) { + mV3SigningExplicitlyEnabled = true; + } else { + mV3SigningExplicitlyDisabled = true; + } + return this; + } + + /** + * Sets whether the APK should be signed even if it is marked as debuggable + * ({@code android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward + * compatibility reasons, the default value of this setting is {@code true}. + * + *

It is dangerous to sign debuggable APKs with production/release keys because Android + * platform loosens security checks for such APKs. For example, arbitrary unauthorized code + * may be executed in the context of such an app by anybody with ADB shell access. + */ + public Builder setDebuggableApkPermitted(boolean permitted) { + mDebuggableApkPermitted = permitted; + return this; + } + + /** + * Sets whether signatures produced by signers other than the ones configured in this engine + * should be copied from the input APK to the output APK. + * + *

By default, signatures of other signers are omitted from the output APK. + */ + public Builder setOtherSignersSignaturesPreserved(boolean preserved) { + mOtherSignersSignaturesPreserved = preserved; + return this; + } + + /** + * Sets the value of the {@code Created-By} field in JAR signature files. + */ + public Builder setCreatedBy(String createdBy) { + if (createdBy == null) { + throw new NullPointerException(); + } + mCreatedBy = createdBy; + return this; + } + + /** + * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This + * structure provides proof of signing certificate rotation linking {@link SignerConfig} + * objects to previous ones. + */ + public Builder setSigningCertificateLineage( + SigningCertificateLineage signingCertificateLineage) { + if (signingCertificateLineage != null) { + mV3SigningEnabled = true; + mSigningCertificateLineage = signingCertificateLineage; + } + return this; + } + } +} diff --git a/app/src/main/java/com/android/apksig/Hints.java b/app/src/main/java/com/android/apksig/Hints.java new file mode 100644 index 0000000..49ef2b0 --- /dev/null +++ b/app/src/main/java/com/android/apksig/Hints.java @@ -0,0 +1,82 @@ +/* + * 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.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; + } + } + + /** + * Create a blob of bytes that PinnerService understands as a + * sequence of byte ranges to pin. + */ + public static byte[] encodeByteRangeList(List 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 parsePinPatterns(byte[] patternBlob) { + ArrayList pinPatterns = new ArrayList<>(); + try { + for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) { + String line = rawLine.replaceFirst("#.*", ""); // # starts a comment + if (!("".equals(line))) { + pinPatterns.add(Pattern.compile(line)); + } + } + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException("UTF-8 must be supported", ex); + } + return pinPatterns; + } +} diff --git a/app/src/main/java/com/android/apksig/SigningCertificateLineage.java b/app/src/main/java/com/android/apksig/SigningCertificateLineage.java new file mode 100644 index 0000000..54340d7 --- /dev/null +++ b/app/src/main/java/com/android/apksig/SigningCertificateLineage.java @@ -0,0 +1,1086 @@ +/* + * 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 static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; + +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.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.v3.V3SchemeSigner; +import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage; +import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage.SigningCertificateNode; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.util.ByteBufferUtils; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.util.RandomAccessFileDataSink; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; +import com.android.apksig.zip.ZipFormatException; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * APK Signer Lineage. + * + *

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. + * + *

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 Application Signing + */ +public class SigningCertificateLineage { + + public final static int MAGIC = 0x3eff39d1; + + private final static int FIRST_VERSION = 1; + + private static final int CURRENT_VERSION = FIRST_VERSION; + + /** accept data from already installed pkg with this cert */ + private static final int PAST_CERT_INSTALLED_DATA = 1; + + /** accept sharedUserId with pkg with this cert */ + private static final int PAST_CERT_SHARED_USER_ID = 2; + + /** grant SIGNATURE permissions to pkgs with this cert */ + private static final int PAST_CERT_PERMISSION = 4; + + /** + * Enable updates back to this certificate. WARNING: this effectively removes any benefit of + * signing certificate changes, since a compromised key could retake control of an app even + * after change, and should only be used if there is a problem encountered when trying to ditch + * an older cert. + */ + private static final int PAST_CERT_ROLLBACK = 8; + + /** + * Preserve authenticator module-based access in AccountManager gated by signing certificate. + */ + private static final int PAST_CERT_AUTH = 16; + + private final int mMinSdkVersion; + + /** + * The signing lineage is just a list of nodes, with the first being the original signing + * certificate and the most recent being the one with which the APK is to actually be signed. + */ + private final List mSigningLineage; + + private SigningCertificateLineage(int minSdkVersion, List list) { + mMinSdkVersion = minSdkVersion; + mSigningLineage = list; + } + + private static SigningCertificateLineage createSigningLineage( + int minSdkVersion, SignerConfig parent, SignerCapabilities parentCapabilities, + SignerConfig child, SignerCapabilities childCapabilities) + throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + SigningCertificateLineage signingCertificateLineage = + new SigningCertificateLineage(minSdkVersion, new ArrayList<>()); + signingCertificateLineage = + signingCertificateLineage.spawnFirstDescendant(parent, parentCapabilities); + return signingCertificateLineage.spawnDescendant(parent, child, childCapabilities); + } + + public static SigningCertificateLineage readFromFile(File file) + throws IOException { + if (file == null) { + throw new NullPointerException("file == null"); + } + RandomAccessFile inputFile = new RandomAccessFile(file, "r"); + return readFromDataSource(DataSources.asDataSource(inputFile)); + } + + public static SigningCertificateLineage readFromDataSource(DataSource dataSource) + throws IOException { + if (dataSource == null) { + throw new NullPointerException("dataSource == null"); + } + ByteBuffer inBuff = dataSource.getByteBuffer(0, (int) dataSource.size()); + inBuff.order(ByteOrder.LITTLE_ENDIAN); + return read(inBuff); + } + + /** + * Extracts a Signing Certificate Lineage from a v3 signer proof-of-rotation attribute. + * + * + * this may not give a complete representation of an APK's signing certificate history, + * since the APK may have multiple signers corresponding to different platform versions. + * Use readFromApkFile to handle this case. + * + * @param attrValue + */ + public static SigningCertificateLineage readFromV3AttributeValue(byte[] attrValue) + throws IOException { + List parsedLineage = + V3SigningCertificateLineage.readSigningCertificateLineage(ByteBuffer.wrap( + attrValue).order(ByteOrder.LITTLE_ENDIAN)); + int minSdkVersion = calculateMinSdkVersion(parsedLineage); + return new SigningCertificateLineage(minSdkVersion, parsedLineage); + } + + /** + * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3 + * signature block of the provided APK File. + * + * @throws IllegalArgumentException if the provided APK does not contain a V3 signature block, + * or if the V3 signature block does not contain a valid lineage. + */ + public static SigningCertificateLineage readFromApkFile(File apkFile) + throws IOException, ApkFormatException { + try (RandomAccessFile f = new RandomAccessFile(apkFile, "r")) { + DataSource apk = DataSources.asDataSource(f, 0, f.length()); + return readFromApkDataSource(apk); + } + } + + /** + * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3 + * signature block of the provided APK DataSource. + * + * @throws IllegalArgumentException if the provided APK does not contain a V3 signature block, + * or if the V3 signature block does not contain a valid lineage. + */ + public static SigningCertificateLineage readFromApkDataSource(DataSource apk) + throws IOException, ApkFormatException { + SignatureInfo signatureInfo; + try { + ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + signatureInfo = + ApkSigningBlockUtils.findSignature(apk, zipSections, + V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result); + } catch (ZipFormatException e) { + throw new ApkFormatException(e.getMessage()); + } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { + throw new IllegalArgumentException( + "The provided APK does not contain a valid V3 signature block."); + } + + // FORMAT: + // * length-prefixed sequence of length-prefixed signers: + // * length-prefixed signed data + // * minSDK + // * maxSDK + // * length-prefixed sequence of length-prefixed signatures + // * length-prefixed public key + ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock); + List lineages = new ArrayList<>(1); + while (signers.hasRemaining()) { + ByteBuffer signer = getLengthPrefixedSlice(signers); + ByteBuffer signedData = getLengthPrefixedSlice(signer); + try { + SigningCertificateLineage lineage = readFromSignedData(signedData); + lineages.add(lineage); + } catch (IllegalArgumentException ignored) { + // The current signer block does not contain a valid lineage, but it is possible + // another block will. + } + } + SigningCertificateLineage result; + if (lineages.isEmpty()) { + throw new IllegalArgumentException( + "The provided APK does not contain a valid lineage."); + } else if (lineages.size() > 1) { + result = consolidateLineages(lineages); + } else { + result = lineages.get(0); + } + return result; + } + + /** + * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the provided + * signed data portion of a signer in a V3 signature block. + * + * @throws IllegalArgumentException if the provided signed data does not contain a valid + * lineage. + */ + public static SigningCertificateLineage readFromSignedData(ByteBuffer signedData) + throws IOException, ApkFormatException { + // FORMAT: + // * length-prefixed sequence of length-prefixed digests: + // * 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 proof-of-rotation structure + // consume the digests through the maxSdkVersion to reach the lineage in the attributes + getLengthPrefixedSlice(signedData); + getLengthPrefixedSlice(signedData); + signedData.getInt(); + signedData.getInt(); + // iterate over the additional attributes adding any lineages to the List + ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData); + List lineages = new ArrayList<>(1); + while (additionalAttributes.hasRemaining()) { + ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes); + int id = attribute.getInt(); + if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) { + byte[] value = ByteBufferUtils.toByteArray(attribute); + SigningCertificateLineage lineage = readFromV3AttributeValue(value); + lineages.add(lineage); + } + } + SigningCertificateLineage result; + // There should only be a single attribute with the lineage, but if there are multiple then + // attempt to consolidate the lineages. + if (lineages.isEmpty()) { + throw new IllegalArgumentException("The signed data does not contain a valid lineage."); + } else if (lineages.size() > 1) { + result = consolidateLineages(lineages); + } else { + result = lineages.get(0); + } + return result; + } + + public void writeToFile(File file) throws IOException { + if (file == null) { + throw new NullPointerException("file == null"); + } + RandomAccessFile outputFile = new RandomAccessFile(file, "rw"); + writeToDataSink(new RandomAccessFileDataSink(outputFile)); + } + + public void writeToDataSink(DataSink dataSink) throws IOException { + if (dataSink == null) { + throw new NullPointerException("dataSink == null"); + } + dataSink.consume(write()); + } + + /** + * Add a new signing certificate to the lineage. This effectively creates a signing certificate + * rotation event, forcing APKs which include this lineage to be signed by the new signer. The + * flags associated with the new signer are set to a default value. + * + * @param parent current signing certificate of the containing APK + * @param child new signing certificate which will sign the APK contents + */ + public SigningCertificateLineage spawnDescendant(SignerConfig parent, SignerConfig child) + throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + if (parent == null || child == null) { + throw new NullPointerException("can't add new descendant to lineage with null inputs"); + } + SignerCapabilities signerCapabilities = new SignerCapabilities.Builder().build(); + return spawnDescendant(parent, child, signerCapabilities); + } + + /** + * Add a new signing certificate to the lineage. This effectively creates a signing certificate + * rotation event, forcing APKs which include this lineage to be signed by the new signer. + * + * @param parent current signing certificate of the containing APK + * @param child new signing certificate which will sign the APK contents + * @param childCapabilities flags + */ + public SigningCertificateLineage spawnDescendant( + SignerConfig parent, SignerConfig child, SignerCapabilities childCapabilities) + throws CertificateEncodingException, InvalidKeyException, + NoSuchAlgorithmException, SignatureException { + if (parent == null) { + throw new NullPointerException("parent == null"); + } + if (child == null) { + throw new NullPointerException("child == null"); + } + if (childCapabilities == null) { + throw new NullPointerException("childCapabilities == null"); + } + if (mSigningLineage.isEmpty()) { + throw new IllegalArgumentException("Cannot spawn descendant signing certificate on an" + + " empty SigningCertificateLineage: no parent node"); + } + + // make sure that the parent matches our newest generation (leaf node/sink) + SigningCertificateNode currentGeneration = mSigningLineage.get(mSigningLineage.size() - 1); + if (!Arrays.equals(currentGeneration.signingCert.getEncoded(), + parent.getCertificate().getEncoded())) { + throw new IllegalArgumentException("SignerConfig Certificate containing private key" + + " to sign the new SigningCertificateLineage record does not match the" + + " existing most recent record"); + } + + // create data to be signed, including the algorithm we're going to use + SignatureAlgorithm signatureAlgorithm = getSignatureAlgorithm(parent); + ByteBuffer prefixedSignedData = ByteBuffer.wrap( + V3SigningCertificateLineage.encodeSignedData( + child.getCertificate(), signatureAlgorithm.getId())); + prefixedSignedData.position(4); + ByteBuffer signedDataBuffer = ByteBuffer.allocate(prefixedSignedData.remaining()); + signedDataBuffer.put(prefixedSignedData); + byte[] signedData = signedDataBuffer.array(); + + // create SignerConfig to do the signing + List certificates = new ArrayList<>(1); + certificates.add(parent.getCertificate()); + ApkSigningBlockUtils.SignerConfig newSignerConfig = + new ApkSigningBlockUtils.SignerConfig(); + newSignerConfig.privateKey = parent.getPrivateKey(); + newSignerConfig.certificates = certificates; + newSignerConfig.signatureAlgorithms = Collections.singletonList(signatureAlgorithm); + + // sign it + List> signatures = + ApkSigningBlockUtils.generateSignaturesOverData(newSignerConfig, signedData); + + // finally, add it to our lineage + SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(signatures.get(0).getFirst()); + byte[] signature = signatures.get(0).getSecond(); + currentGeneration.sigAlgorithm = sigAlgorithm; + SigningCertificateNode childNode = + new SigningCertificateNode( + child.getCertificate(), sigAlgorithm, null, + signature, childCapabilities.getFlags()); + List lineageCopy = new ArrayList<>(mSigningLineage); + lineageCopy.add(childNode); + return new SigningCertificateLineage(mMinSdkVersion, lineageCopy); + } + + /** + * The number of signing certificates in the lineage, including the current signer, which means + * this value can also be used to V2determine the number of signing certificate rotations by + * subtracting 1. + */ + public int size() { + return mSigningLineage.size(); + } + + private SignatureAlgorithm getSignatureAlgorithm(SignerConfig parent) + throws InvalidKeyException { + PublicKey publicKey = parent.getCertificate().getPublicKey(); + + // TODO switch to one signature algorithm selection, or add support for multiple algorithms + List algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms( + publicKey, mMinSdkVersion, false /* padding support */); + return algorithms.get(0); + } + + private SigningCertificateLineage spawnFirstDescendant( + SignerConfig parent, SignerCapabilities signerCapabilities) { + if (!mSigningLineage.isEmpty()) { + throw new IllegalStateException("SigningCertificateLineage already has its first node"); + } + + // check to make sure that the public key for the first node is acceptable for our minSdk + try { + getSignatureAlgorithm(parent); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("Algorithm associated with first signing certificate" + + " invalid on desired platform versions", e); + } + + // create "fake" signed data (there will be no signature over it, since there is no parent + SigningCertificateNode firstNode = new SigningCertificateNode( + parent.getCertificate(), null, null, new byte[0], signerCapabilities.getFlags()); + return new SigningCertificateLineage(mMinSdkVersion, Collections.singletonList(firstNode)); + } + + private static SigningCertificateLineage read(ByteBuffer inputByteBuffer) + throws IOException { + ApkSigningBlockUtils.checkByteOrderLittleEndian(inputByteBuffer); + if (inputByteBuffer.remaining() < 8) { + throw new IllegalArgumentException( + "Improper SigningCertificateLineage format: insufficient data for header."); + } + + if (inputByteBuffer.getInt() != MAGIC) { + throw new IllegalArgumentException( + "Improper SigningCertificateLineage format: MAGIC header mismatch."); + } + return read(inputByteBuffer, inputByteBuffer.getInt()); + } + + private static SigningCertificateLineage read(ByteBuffer inputByteBuffer, int version) + throws IOException { + switch (version) { + case FIRST_VERSION: + try { + List nodes = + V3SigningCertificateLineage.readSigningCertificateLineage( + getLengthPrefixedSlice(inputByteBuffer)); + int minSdkVersion = calculateMinSdkVersion(nodes); + return new SigningCertificateLineage(minSdkVersion, nodes); + } catch (ApkFormatException e) { + // unable to get a proper length-prefixed lineage slice + throw new IOException("Unable to read list of signing certificate nodes in " + + "SigningCertificateLineage", e); + } + default: + throw new IllegalArgumentException( + "Improper SigningCertificateLineage format: unrecognized version."); + } + } + + private static int calculateMinSdkVersion(List nodes) { + if (nodes == null) { + throw new IllegalArgumentException("Can't calculate minimum SDK version of null nodes"); + } + int minSdkVersion = AndroidSdkVersion.P; // lineage introduced in P + for (SigningCertificateNode node : nodes) { + if (node.sigAlgorithm != null) { + int nodeMinSdkVersion = node.sigAlgorithm.getMinSdkVersion(); + if (nodeMinSdkVersion > minSdkVersion) { + minSdkVersion = nodeMinSdkVersion; + } + } + } + return minSdkVersion; + } + + private ByteBuffer write() { + byte[] encodedLineage = + V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage); + int payloadSize = 4 + 4 + 4 + encodedLineage.length; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(MAGIC); + result.putInt(CURRENT_VERSION); + result.putInt(encodedLineage.length); + result.put(encodedLineage); + result.flip(); + return result; + } + + public byte[] generateV3SignerAttribute() { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID + // * bytes: value - encoded V3 SigningCertificateLineage + byte[] encodedLineage = + V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage); + int payloadSize = 4 + 4 + encodedLineage.length; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(4 + encodedLineage.length); + result.putInt(V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID); + result.put(encodedLineage); + return result.array(); + } + + public List sortSignerConfigs( + List signerConfigs) { + if (signerConfigs == null) { + throw new NullPointerException("signerConfigs == null"); + } + + // not the most elegant sort, but we expect signerConfigs to be quite small (1 or 2 signers + // in most cases) and likely already sorted, so not worth the overhead of doing anything + // fancier + List sortedSignerConfigs = + new ArrayList<>(signerConfigs.size()); + for (int i = 0; i < mSigningLineage.size(); i++) { + for (int j = 0; j < signerConfigs.size(); j++) { + DefaultApkSignerEngine.SignerConfig config = signerConfigs.get(j); + if (mSigningLineage.get(i).signingCert.equals(config.getCertificates().get(0))) { + sortedSignerConfigs.add(config); + break; + } + } + } + if (sortedSignerConfigs.size() != signerConfigs.size()) { + throw new IllegalArgumentException("SignerConfigs supplied which are not present in the" + + " SigningCertificateLineage"); + } + return sortedSignerConfigs; + } + + /** + * Returns the SignerCapabilities for the signer in the lineage that matches the provided + * config. + */ + public SignerCapabilities getSignerCapabilities(SignerConfig config) { + if (config == null) { + throw new NullPointerException("config == null"); + } + + X509Certificate cert = config.getCertificate(); + return getSignerCapabilities(cert); + } + + /** + * Returns the SignerCapabilities for the signer in the lineage that matches the provided + * certificate. + */ + public SignerCapabilities getSignerCapabilities(X509Certificate cert) { + if (cert == null) { + throw new NullPointerException("cert == null"); + } + + for (int i = 0; i < mSigningLineage.size(); i++) { + SigningCertificateNode lineageNode = mSigningLineage.get(i); + if (lineageNode.signingCert.equals(cert)) { + int flags = lineageNode.flags; + return new SignerCapabilities.Builder(flags).build(); + } + } + + // the provided signer certificate was not found in the lineage + throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN() + + ") not found in the SigningCertificateLineage"); + } + + /** + * Updates the SignerCapabilities for the signer in the lineage that matches the provided + * config. Only those capabilities that have been modified through the setXX methods will be + * updated for the signer to prevent unset default values from being applied. + */ + public void updateSignerCapabilities(SignerConfig config, SignerCapabilities capabilities) { + if (config == null) { + throw new NullPointerException("config == null"); + } + + X509Certificate cert = config.getCertificate(); + for (int i = 0; i < mSigningLineage.size(); i++) { + SigningCertificateNode lineageNode = mSigningLineage.get(i); + if (lineageNode.signingCert.equals(cert)) { + int flags = lineageNode.flags; + SignerCapabilities newCapabilities = new SignerCapabilities.Builder( + flags).setCallerConfiguredCapabilities(capabilities).build(); + lineageNode.flags = newCapabilities.getFlags(); + return; + } + } + + // the provided signer config was not found in the lineage + throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN() + + ") not found in the SigningCertificateLineage"); + } + + /** + * Returns a list containing all of the certificates in the lineage. + */ + public List getCertificatesInLineage() { + List certs = new ArrayList<>(); + for (int i = 0; i < mSigningLineage.size(); i++) { + X509Certificate cert = mSigningLineage.get(i).signingCert; + certs.add(cert); + } + return certs; + } + + /** + * Returns {@code true} if the specified config is in the lineage. + */ + public boolean isSignerInLineage(SignerConfig config) { + if (config == null) { + throw new NullPointerException("config == null"); + } + + X509Certificate cert = config.getCertificate(); + return isCertificateInLineage(cert); + } + + /** + * Returns {@code true} if the specified certificate is in the lineage. + */ + public boolean isCertificateInLineage(X509Certificate cert) { + if (cert == null) { + throw new NullPointerException("cert == null"); + } + + for (int i = 0; i < mSigningLineage.size(); i++) { + if (mSigningLineage.get(i).signingCert.equals(cert)) { + return true; + } + } + return false; + } + + private static int calculateDefaultFlags() { + return PAST_CERT_INSTALLED_DATA | PAST_CERT_PERMISSION + | PAST_CERT_SHARED_USER_ID | PAST_CERT_AUTH; + } + + /** + * Returns a new SigingCertificateLineage which terminates at the node corresponding to the + * given certificate. This is useful in the event of rotating to a new signing algorithm that + * is only supported on some platform versions. It enables a v3 signature to be generated using + * this signing certificate and the shortened proof-of-rotation record from this sub lineage in + * conjunction with the appropriate SDK version values. + * + * @param x509Certificate the signing certificate for which to search + * @return A new SigningCertificateLineage if the given certificate is present. + * + * @throws IllegalArgumentException if the provided certificate is not in the lineage. + */ + public SigningCertificateLineage getSubLineage(X509Certificate x509Certificate) { + if (x509Certificate == null) { + throw new NullPointerException("x509Certificate == null"); + } + for (int i = 0; i < mSigningLineage.size(); i++) { + if (mSigningLineage.get(i).signingCert.equals(x509Certificate)) { + return new SigningCertificateLineage( + mMinSdkVersion, new ArrayList<>(mSigningLineage.subList(0, i + 1))); + } + } + + // looks like we didn't find the cert, + throw new IllegalArgumentException("Certificate not found in SigningCertificateLineage"); + } + + /** + * Consolidates all of the lineages found in an APK into one lineage, which is the longest one. + * In so doing, it also checks that all of the smaller lineages are contained in the largest, + * and that they properly cover the desired platform ranges. + * + * An APK may contain multiple lineages, one for each signer, which correspond to different + * supported platform versions. In this event, the lineage(s) from the earlier platform + * version(s) need to be present in the most recent (longest) one to make sure that when a + * platform version changes. + * + * This does not verify that the largest lineage corresponds to the most recent supported + * platform version. That check requires is performed during v3 verification. + */ + public static SigningCertificateLineage consolidateLineages( + List lineages) { + if (lineages == null || lineages.isEmpty()) { + return null; + } + int largestIndex = 0; + int maxSize = 0; + + // determine the longest chain + for (int i = 0; i < lineages.size(); i++) { + int curSize = lineages.get(i).size(); + if (curSize > maxSize) { + largestIndex = i; + maxSize = curSize; + } + } + + List largestList = lineages.get(largestIndex).mSigningLineage; + // make sure all other lineages fit into this one, with the same capabilities + for (int i = 0; i < lineages.size(); i++) { + if (i == largestIndex) { + continue; + } + List underTest = lineages.get(i).mSigningLineage; + if (!underTest.equals(largestList.subList(0, underTest.size()))) { + throw new IllegalArgumentException("Inconsistent SigningCertificateLineages. " + + "Not all lineages are subsets of each other."); + } + } + + // if we've made it this far, they all check out, so just return the largest + return lineages.get(largestIndex); + } + + /** + * Representation of the capabilities the APK would like to grant to its old signing + * certificates. The {@code SigningCertificateLineage} provides two conceptual data structures. + * 1) proof of rotation - Evidence that other parties can trust an APK's current signing + * certificate if they trust an older one in this lineage + * 2) self-trust - certain capabilities may have been granted by an APK to other parties based + * on its own signing certificate. When it changes its signing certificate it may want to + * allow the other parties to retain those capabilities. + * {@code SignerCapabilties} provides a representation of the second structure. + * + *

Use {@link Builder} to obtain configuration instances. + */ + public static class SignerCapabilities { + private final int mFlags; + + private final int mCallerConfiguredFlags; + + private SignerCapabilities(int flags) { + this(flags, 0); + } + + private SignerCapabilities(int flags, int callerConfiguredFlags) { + mFlags = flags; + mCallerConfiguredFlags = callerConfiguredFlags; + } + + private int getFlags() { + return mFlags; + } + + /** + * Returns {@code true} if the capabilities of this object match those of the provided + * object. + */ + public boolean equals(SignerCapabilities other) { + return this.mFlags == other.mFlags; + } + + /** + * Returns {@code true} if this object has the installed data capability. + */ + public boolean hasInstalledData() { + return (mFlags & PAST_CERT_INSTALLED_DATA) != 0; + } + + /** + * Returns {@code true} if this object has the shared UID capability. + */ + public boolean hasSharedUid() { + return (mFlags & PAST_CERT_SHARED_USER_ID) != 0; + } + + /** + * Returns {@code true} if this object has the permission capability. + */ + public boolean hasPermission() { + return (mFlags & PAST_CERT_PERMISSION) != 0; + } + + /** + * Returns {@code true} if this object has the rollback capability. + */ + public boolean hasRollback() { + return (mFlags & PAST_CERT_ROLLBACK) != 0; + } + + /** + * Returns {@code true} if this object has the auth capability. + */ + public boolean hasAuth() { + return (mFlags & PAST_CERT_AUTH) != 0; + } + + /** + * Builder of {@link SignerCapabilities} instances. + */ + public static class Builder { + private int mFlags; + + private int mCallerConfiguredFlags; + + /** + * Constructs a new {@code Builder}. + */ + public Builder() { + mFlags = calculateDefaultFlags(); + } + + /** + * Constructs a new {@code Builder} with the initial capabilities set to the provided + * flags. + */ + public Builder(int flags) { + mFlags = flags; + } + + /** + * Set the {@code PAST_CERT_INSTALLED_DATA} flag in this capabilities object. This flag + * is used by the platform to determine if installed data associated with previous + * signing certificate should be trusted. In particular, this capability is required to + * perform signing certificate rotation during an upgrade on-device. Without it, the + * platform will not permit the app data from the old signing certificate to + * propagate to the new version. Typically, this flag should be set to enable signing + * certificate rotation, and may be unset later when the app developer is satisfied that + * their install base is as migrated as it will be. + */ + public Builder setInstalledData(boolean enabled) { + mCallerConfiguredFlags |= PAST_CERT_INSTALLED_DATA; + if (enabled) { + mFlags |= PAST_CERT_INSTALLED_DATA; + } else { + mFlags &= ~PAST_CERT_INSTALLED_DATA; + } + return this; + } + + /** + * Set the {@code PAST_CERT_SHARED_USER_ID} flag in this capabilities object. This flag + * is used by the platform to determine if this app is willing to be sharedUid with + * other apps which are still signed with the associated signing certificate. This is + * useful in situations where sharedUserId apps would like to change their signing + * certificate, but can't guarantee the order of updates to those apps. + */ + public Builder setSharedUid(boolean enabled) { + mCallerConfiguredFlags |= PAST_CERT_SHARED_USER_ID; + if (enabled) { + mFlags |= PAST_CERT_SHARED_USER_ID; + } else { + mFlags &= ~PAST_CERT_SHARED_USER_ID; + } + return this; + } + + /** + * Set the {@code PAST_CERT_PERMISSION} flag in this capabilities object. This flag + * is used by the platform to determine if this app is willing to grant SIGNATURE + * permissions to apps signed with the associated signing certificate. Without this + * capability, an application signed with the older certificate will not be granted the + * SIGNATURE permissions defined by this app. In addition, if multiple apps define the + * same SIGNATURE permission, the second one the platform sees will not be installable + * if this capability is not set and the signing certificates differ. + */ + public Builder setPermission(boolean enabled) { + mCallerConfiguredFlags |= PAST_CERT_PERMISSION; + if (enabled) { + mFlags |= PAST_CERT_PERMISSION; + } else { + mFlags &= ~PAST_CERT_PERMISSION; + } + return this; + } + + /** + * Set the {@code PAST_CERT_ROLLBACK} flag in this capabilities object. This flag + * is used by the platform to determine if this app is willing to upgrade to a new + * version that is signed by one of its past signing certificates. + * + * WARNING: this effectively removes any benefit of signing certificate changes, + * since a compromised key could retake control of an app even after change, and should + * only be used if there is a problem encountered when trying to ditch an older cert + * + */ + public Builder setRollback(boolean enabled) { + mCallerConfiguredFlags |= PAST_CERT_ROLLBACK; + if (enabled) { + mFlags |= PAST_CERT_ROLLBACK; + } else { + mFlags &= ~PAST_CERT_ROLLBACK; + } + return this; + } + + /** + * Set the {@code PAST_CERT_AUTH} flag in this capabilities object. This flag + * is used by the platform to determine whether or not privileged access based on + * authenticator module signing certificates should be granted. + */ + public Builder setAuth(boolean enabled) { + mCallerConfiguredFlags |= PAST_CERT_AUTH; + if (enabled) { + mFlags |= PAST_CERT_AUTH; + } else { + mFlags &= ~PAST_CERT_AUTH; + } + return this; + } + + /** + * Applies the capabilities that were explicitly set in the provided capabilities object + * to this builder. Any values that were not set will not be applied to this builder + * to prevent unintentinoally setting a capability back to a default value. + */ + public Builder setCallerConfiguredCapabilities(SignerCapabilities capabilities) { + // The mCallerConfiguredFlags should have a bit set for each capability that was + // set by a caller. If a capability was explicitly set then the corresponding bit + // in mCallerConfiguredFlags should be set. This allows the provided capabilities + // to take effect for those set by the caller while those that were not set will + // be cleared by the bitwise and and the initial value for the builder will remain. + mFlags = (mFlags & ~capabilities.mCallerConfiguredFlags) | + (capabilities.mFlags & capabilities.mCallerConfiguredFlags); + return this; + } + + /** + * Returns a new {@code SignerConfig} instance configured based on the configuration of + * this builder. + */ + public SignerCapabilities build() { + return new SignerCapabilities(mFlags, mCallerConfiguredFlags); + } + } + } + + /** + * Configuration of a signer. Used to add a new entry to the {@link SigningCertificateLineage} + * + *

Use {@link Builder} to obtain configuration instances. + */ + public static class SignerConfig { + private final PrivateKey mPrivateKey; + private final X509Certificate mCertificate; + + private SignerConfig( + PrivateKey privateKey, + X509Certificate certificate) { + mPrivateKey = privateKey; + mCertificate = certificate; + } + + /** + * Returns the signing key of this signer. + */ + public PrivateKey getPrivateKey() { + return mPrivateKey; + } + + /** + * Returns the certificate(s) of this signer. The first certificate's public key corresponds + * to this signer's private key. + */ + public X509Certificate getCertificate() { + return mCertificate; + } + + /** + * Builder of {@link SignerConfig} instances. + */ + public static class Builder { + private final PrivateKey mPrivateKey; + private final X509Certificate mCertificate; + + /** + * Constructs a new {@code Builder}. + * + * @param privateKey signing key + * @param certificate the X.509 certificate with a subject public key of the + * {@code privateKey}. + */ + public Builder( + PrivateKey privateKey, + X509Certificate certificate) { + mPrivateKey = privateKey; + mCertificate = certificate; + } + + /** + * Returns a new {@code SignerConfig} instance configured based on the configuration of + * this builder. + */ + public SignerConfig build() { + return new SignerConfig( + mPrivateKey, + mCertificate); + } + } + } + + /** + * Builder of {@link SigningCertificateLineage} instances. + */ + public static class Builder { + private final SignerConfig mOriginalSignerConfig; + private final SignerConfig mNewSignerConfig; + private SignerCapabilities mOriginalCapabilities; + private SignerCapabilities mNewCapabilities; + private int mMinSdkVersion; + /** + * Constructs a new {@code Builder}. + * + * @param originalSignerConfig first signer in this lineage, parent of the next + * @param newSignerConfig new signer in the lineage; the new signing key that the APK will + * use + */ + public Builder( + SignerConfig originalSignerConfig, + SignerConfig newSignerConfig) { + if (originalSignerConfig == null || newSignerConfig == null) { + throw new NullPointerException("Can't pass null SignerConfigs when constructing a " + + "new SigningCertificateLineage"); + } + mOriginalSignerConfig = originalSignerConfig; + mNewSignerConfig = newSignerConfig; + } + + /** + * Sets the minimum Android platform version (API Level) on which this lineage is expected + * to validate. It is possible that newer signers in the lineage may not be recognized on + * the given platform, but as long as an older signer is, the lineage can still be used to + * sign an APK for the given platform. + * + * By default, this value is set to the value for the + * P release, since this structure was created for that release, and will also be set to + * that value if a smaller one is specified. + */ + public Builder setMinSdkVersion(int minSdkVersion) { + mMinSdkVersion = minSdkVersion; + return this; + } + + /** + * Sets capabilities to give {@code mOriginalSignerConfig}. These capabilities allow an + * older signing certificate to still be used in some situations on the platform even though + * the APK is now being signed by a newer signing certificate. + */ + public Builder setOriginalCapabilities(SignerCapabilities signerCapabilities) { + if (signerCapabilities == null) { + throw new NullPointerException("signerCapabilities == null"); + } + mOriginalCapabilities = signerCapabilities; + return this; + } + + /** + * Sets capabilities to give {@code mNewSignerConfig}. These capabilities allow an + * older signing certificate to still be used in some situations on the platform even though + * the APK is now being signed by a newer signing certificate. By default, the new signer + * will have all capabilities, so when first switching to a new signing certificate, these + * capabilities have no effect, but they will act as the default level of trust when moving + * to a new signing certificate. + */ + public Builder setNewCapabilities(SignerCapabilities signerCapabilities) { + if (signerCapabilities == null) { + throw new NullPointerException("signerCapabilities == null"); + } + mNewCapabilities = signerCapabilities; + return this; + } + + public SigningCertificateLineage build() + throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + if (mMinSdkVersion < AndroidSdkVersion.P) { + mMinSdkVersion = AndroidSdkVersion.P; + } + + if (mOriginalCapabilities == null) { + mOriginalCapabilities = new SignerCapabilities.Builder().build(); + } + + if (mNewCapabilities == null) { + mNewCapabilities = new SignerCapabilities.Builder().build(); + } + + return createSigningLineage( + mMinSdkVersion, mOriginalSignerConfig, mOriginalCapabilities, + mNewSignerConfig, mNewCapabilities); + } + } +} diff --git a/app/src/main/java/com/android/apksig/apk/ApkFormatException.java b/app/src/main/java/com/android/apksig/apk/ApkFormatException.java new file mode 100644 index 0000000..a780134 --- /dev/null +++ b/app/src/main/java/com/android/apksig/apk/ApkFormatException.java @@ -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); + } +} diff --git a/app/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java b/app/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java new file mode 100644 index 0000000..fd961d5 --- /dev/null +++ b/app/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java @@ -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); + } +} diff --git a/app/src/main/java/com/android/apksig/apk/ApkUtils.java b/app/src/main/java/com/android/apksig/apk/ApkUtils.java new file mode 100644 index 0000000..135d815 --- /dev/null +++ b/app/src/main/java/com/android/apksig/apk/ApkUtils.java @@ -0,0 +1,604 @@ +/* + * 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.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"; + + 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 { + Pair 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); + } + + /** + * Information about the ZIP sections of an APK. + */ + public static class ZipSections { + private final long mCentralDirectoryOffset; + private final long mCentralDirectorySizeBytes; + private final int mCentralDirectoryRecordCount; + private final long mEocdOffset; + private final ByteBuffer mEocd; + + public ZipSections( + long centralDirectoryOffset, + long centralDirectorySizeBytes, + int centralDirectoryRecordCount, + long eocdOffset, + ByteBuffer eocd) { + mCentralDirectoryOffset = centralDirectoryOffset; + mCentralDirectorySizeBytes = centralDirectorySizeBytes; + mCentralDirectoryRecordCount = centralDirectoryRecordCount; + mEocdOffset = eocdOffset; + mEocd = eocd; + } + + /** + * Returns the start offset of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectoryOffset() { + return mCentralDirectoryOffset; + } + + /** + * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectorySizeBytes() { + return mCentralDirectorySizeBytes; + } + + /** + * Returns the number of records in the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public int getZipCentralDirectoryRecordCount() { + return mCentralDirectoryRecordCount; + } + + /** + * Returns the start offset of the ZIP End of Central Directory record. The record extends + * until the very end of the APK. + */ + public long getZipEndOfCentralDirectoryOffset() { + return mEocdOffset; + } + + /** + * Returns the contents of the ZIP End of Central Directory. + */ + public ByteBuffer getZipEndOfCentralDirectory() { + return mEocd; + } + } + + /** + * 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); + } + + // 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 APK Signature Scheme v2 + */ + 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; + } + } + + /** + * 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 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; + + /** + * 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[] 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> CODENAME_FIRST_CHAR_COMPARATOR = + new ByFirstComparator(); + + private static class ByFirstComparator implements Comparator> { + @Override + public int compare(Pair o1, Pair o2) { + char c1 = o1.getFirst(); + char c2 = o2.getFirst(); + return c1 - c2; + } + } + } + + /** + * Returns the API Level corresponding to the provided platform codename. + * + *

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[] 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 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); + } + } +} diff --git a/app/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java b/app/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java new file mode 100644 index 0000000..e30bc35 --- /dev/null +++ b/app/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java @@ -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; + } +} diff --git a/app/src/main/java/com/android/apksig/apk/MinSdkVersionException.java b/app/src/main/java/com/android/apksig/apk/MinSdkVersionException.java new file mode 100644 index 0000000..c4aad08 --- /dev/null +++ b/app/src/main/java/com/android/apksig/apk/MinSdkVersionException.java @@ -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); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java b/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java new file mode 100644 index 0000000..bc5a457 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java @@ -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}. + * + *

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 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 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); + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/app/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java new file mode 100644 index 0000000..cc69af3 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java @@ -0,0 +1,1356 @@ +/* + * 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 com.android.apksig.ApkVerifier; +import com.android.apksig.SigningCertificateLineage; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkSigningBlockNotFoundException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.util.ByteBufferDataSource; +import com.android.apksig.internal.util.ChainedDataSource; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.util.VerityTreeBuilder; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSinks; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; + +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.DigestException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +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.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class ApkSigningBlockUtils { + + private static final char[] HEX_DIGITS = "01234567890abcdef".toCharArray(); + private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; + public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096; + public static final byte[] APK_SIGNING_BLOCK_MAGIC = + new byte[] { + 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, + 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, + }; + private static final int VERITY_PADDING_BLOCK_ID = 0x42726577; + + 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; + + + /** + * 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); + } + } + + + + /** + * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the + * APK and comparing them against the digests listed in APK Signing Block. The expected digests + * are taken from {@code SignerInfos} of the provided {@code result}. + * + *

This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on Android. No errors are added to the {@code result} if the APK's + * integrity is expected to verify on Android for each algorithm in + * {@code contentDigestAlgorithms}. + * + *

The reason this method is currently not parameterized by a + * {@code [minSdkVersion, maxSdkVersion]} range is that up until now content digest algorithms + * exhibit the same behavior on all Android platform versions. + */ + public static void verifyIntegrity( + RunnablesExecutor executor, + DataSource beforeApkSigningBlock, + DataSource centralDir, + ByteBuffer eocd, + Set contentDigestAlgorithms, + Result result) throws IOException, NoSuchAlgorithmException { + if (contentDigestAlgorithms.isEmpty()) { + // This should never occur because this method is invoked once at least one signature + // is verified, meaning at least one content digest is known. + throw new RuntimeException("No content digests found"); + } + + // For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be + // treated as though its Central Directory offset points to the start of APK Signing Block. + // We thus modify the EoCD accordingly. + ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining()); + int eocdSavedPos = eocd.position(); + modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); + modifiedEocd.put(eocd); + modifiedEocd.flip(); + + // restore eocd to position prior to modification in case it is to be used elsewhere + eocd.position(eocdSavedPos); + ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size()); + Map actualContentDigests; + try { + actualContentDigests = + computeContentDigests( + executor, + contentDigestAlgorithms, + beforeApkSigningBlock, + centralDir, + new ByteBufferDataSource(modifiedEocd)); + // Special checks for the verity algorithm requirements. + if (actualContentDigests.containsKey(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) { + if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) { + throw new RuntimeException( + "APK Signing Block is not aligned on 4k boundary: " + + beforeApkSigningBlock.size()); + } + + long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd); + long signingBlockSize = centralDirOffset - beforeApkSigningBlock.size(); + if (signingBlockSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) { + throw new RuntimeException( + "APK Signing Block size is not multiple of page size: " + + signingBlockSize); + } + } + } catch (DigestException e) { + throw new RuntimeException("Failed to compute content digests", e); + } + if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) { + throw new RuntimeException( + "Mismatch between sets of requested and computed content digests" + + " . Requested: " + contentDigestAlgorithms + + ", computed: " + actualContentDigests.keySet()); + } + + // Compare digests computed over the rest of APK against the corresponding expected digests + // in signer blocks. + for (Result.SignerInfo signerInfo : result.signers) { + for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) { + SignatureAlgorithm signatureAlgorithm = + SignatureAlgorithm.findById(expected.getSignatureAlgorithmId()); + if (signatureAlgorithm == null) { + continue; + } + ContentDigestAlgorithm contentDigestAlgorithm = + signatureAlgorithm.getContentDigestAlgorithm(); + // if the current digest algorithm is not in the list provided by the caller then + // ignore it; the signer may contain digests not recognized by the specified SDK + // range. + if (!contentDigestAlgorithms.contains(contentDigestAlgorithm)) { + continue; + } + byte[] expectedDigest = expected.getValue(); + byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm); + if (!Arrays.equals(expectedDigest, actualDigest)) { + if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) { + signerInfo.addError( + ApkVerifier.Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY, + contentDigestAlgorithm, + toHex(expectedDigest), + toHex(actualDigest)); + } else if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3) { + signerInfo.addError( + ApkVerifier.Issue.V3_SIG_APK_DIGEST_DID_NOT_VERIFY, + contentDigestAlgorithm, + toHex(expectedDigest), + toHex(actualDigest)); + } + continue; + } + signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest); + } + } + } + + public static ByteBuffer findApkSignatureSchemeBlock( + ByteBuffer apkSigningBlock, + int blockId, + Result result) 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 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 get method for reading {@code size} number of bytes from the current + * position of this buffer. + * + *

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 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 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 Map computeContentDigests( + RunnablesExecutor executor, + Set digestAlgorithms, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd) throws IOException, NoSuchAlgorithmException, DigestException { + Map contentDigests = new HashMap<>(); + Set oneMbChunkBasedAlgorithm = digestAlgorithms.stream() + .filter(a -> a == ContentDigestAlgorithm.CHUNKED_SHA256 || + a == ContentDigestAlgorithm.CHUNKED_SHA512) + .collect(Collectors.toSet()); + computeOneMbChunkContentDigests( + executor, + oneMbChunkBasedAlgorithm, + new DataSource[] { beforeCentralDir, centralDir, eocd }, + contentDigests); + + if (digestAlgorithms.contains(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) { + computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests); + } + return contentDigests; + } + + static void computeOneMbChunkContentDigests( + Set digestAlgorithms, + DataSource[] contents, + Map outputContentDigests) + throws IOException, NoSuchAlgorithmException, DigestException { + // For each digest algorithm the result is computed as follows: + // 1. Each segment of contents is split into consecutive chunks of 1 MB in size. + // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB. + // No chunks are produced for empty (zero length) segments. + // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's + // length in bytes (uint32 little-endian) and the chunk's contents. + // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of + // chunks (uint32 little-endian) and the concatenation of digests of chunks of all + // segments in-order. + + long chunkCountLong = 0; + for (DataSource input : contents) { + chunkCountLong += + getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + } + if (chunkCountLong > Integer.MAX_VALUE) { + throw new DigestException("Input too long: " + chunkCountLong + " chunks"); + } + int chunkCount = (int) chunkCountLong; + + ContentDigestAlgorithm[] digestAlgorithmsArray = + digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]); + MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length]; + byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][]; + int[] digestOutputSizes = new int[digestAlgorithmsArray.length]; + for (int i = 0; i < digestAlgorithmsArray.length; i++) { + ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i]; + int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes(); + digestOutputSizes[i] = digestOutputSizeBytes; + byte[] concatenationOfChunkCountAndChunkDigests = + new byte[5 + chunkCount * digestOutputSizeBytes]; + concatenationOfChunkCountAndChunkDigests[0] = 0x5a; + setUnsignedInt32LittleEndian( + chunkCount, concatenationOfChunkCountAndChunkDigests, 1); + digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests; + String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm(); + mds[i] = MessageDigest.getInstance(jcaAlgorithm); + } + + DataSink mdSink = DataSinks.asDataSink(mds); + byte[] chunkContentPrefix = new byte[5]; + chunkContentPrefix[0] = (byte) 0xa5; + int chunkIndex = 0; + // Optimization opportunity: digests of chunks can be computed in parallel. However, + // determining the number of computations to be performed in parallel is non-trivial. This + // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched + // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU + // cores, load on the system from other threads of execution and other processes, size of + // input. + // For now, we compute these digests sequentially and thus have the luxury of improving + // performance by writing the digest of each chunk into a pre-allocated buffer at exactly + // the right position. This avoids unnecessary allocations, copying, and enables the final + // digest to be more efficient because it's presented with all of its input in one go. + for (DataSource input : contents) { + long inputOffset = 0; + long inputRemaining = input.size(); + while (inputRemaining > 0) { + int chunkSize = + (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1); + for (int i = 0; i < mds.length; i++) { + mds[i].update(chunkContentPrefix); + } + try { + input.feed(inputOffset, chunkSize, mdSink); + } catch (IOException e) { + throw new IOException("Failed to read chunk #" + chunkIndex, e); + } + for (int i = 0; i < digestAlgorithmsArray.length; i++) { + MessageDigest md = mds[i]; + byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; + int expectedDigestSizeBytes = digestOutputSizes[i]; + int actualDigestSizeBytes = + md.digest( + concatenationOfChunkCountAndChunkDigests, + 5 + chunkIndex * expectedDigestSizeBytes, + expectedDigestSizeBytes); + if (actualDigestSizeBytes != expectedDigestSizeBytes) { + throw new RuntimeException( + "Unexpected output size of " + md.getAlgorithm() + + " digest: " + actualDigestSizeBytes); + } + } + inputOffset += chunkSize; + inputRemaining -= chunkSize; + chunkIndex++; + } + } + + for (int i = 0; i < digestAlgorithmsArray.length; i++) { + ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i]; + byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; + MessageDigest md = mds[i]; + byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests); + outputContentDigests.put(digestAlgorithm, digest); + } + } + + static void computeOneMbChunkContentDigests( + RunnablesExecutor executor, + Set digestAlgorithms, + DataSource[] contents, + Map outputContentDigests) + throws NoSuchAlgorithmException, DigestException { + long chunkCountLong = 0; + for (DataSource input : contents) { + chunkCountLong += + getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + } + if (chunkCountLong > Integer.MAX_VALUE) { + throw new DigestException("Input too long: " + chunkCountLong + " chunks"); + } + int chunkCount = (int) chunkCountLong; + + List chunkDigestsList = new ArrayList<>(digestAlgorithms.size()); + for (ContentDigestAlgorithm algorithms : digestAlgorithms) { + chunkDigestsList.add(new ChunkDigests(algorithms, chunkCount)); + } + + ChunkSupplier chunkSupplier = new ChunkSupplier(contents); + executor.execute(() -> new ChunkDigester(chunkSupplier, chunkDigestsList)); + + // Compute and write out final digest for each algorithm. + for (ChunkDigests chunkDigests : chunkDigestsList) { + MessageDigest messageDigest = chunkDigests.createMessageDigest(); + outputContentDigests.put( + chunkDigests.algorithm, + messageDigest.digest(chunkDigests.concatOfDigestsOfChunks)); + } + } + + private static class ChunkDigests { + private final ContentDigestAlgorithm algorithm; + private final int digestOutputSize; + private final byte[] concatOfDigestsOfChunks; + + private ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount) { + this.algorithm = algorithm; + digestOutputSize = this.algorithm.getChunkDigestOutputSizeBytes(); + concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize]; + + // Fill the initial values of the concatenated digests of chunks, which is + // {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}. + concatOfDigestsOfChunks[0] = 0x5a; + setUnsignedInt32LittleEndian(chunkCount, concatOfDigestsOfChunks, 1); + } + + private MessageDigest createMessageDigest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance(algorithm.getJcaMessageDigestAlgorithm()); + } + + private int getOffset(int chunkIndex) { + return 1 + 4 + chunkIndex * digestOutputSize; + } + } + + /** + * A per-thread digest worker. + */ + private static class ChunkDigester implements Runnable { + private final ChunkSupplier dataSupplier; + private final List chunkDigests; + private final List messageDigests; + private final DataSink mdSink; + + private ChunkDigester(ChunkSupplier dataSupplier, List chunkDigests) { + this.dataSupplier = dataSupplier; + this.chunkDigests = chunkDigests; + messageDigests = new ArrayList<>(chunkDigests.size()); + for (ChunkDigests chunkDigest : chunkDigests) { + try { + messageDigests.add(chunkDigest.createMessageDigest()); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } + } + mdSink = DataSinks.asDataSink(messageDigests.toArray(new MessageDigest[0])); + } + + @Override + public void run() { + byte[] chunkContentPrefix = new byte[5]; + chunkContentPrefix[0] = (byte) 0xa5; + + try { + for (ChunkSupplier.Chunk chunk = dataSupplier.get(); + chunk != null; + chunk = dataSupplier.get()) { + long size = chunk.dataSource.size(); + if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) { + throw new RuntimeException("Chunk size greater than expected: " + size); + } + + // First update with the chunk prefix. + setUnsignedInt32LittleEndian((int)size, chunkContentPrefix, 1); + mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length); + + // Then update with the chunk data. + chunk.dataSource.feed(0, size, mdSink); + + // Now finalize chunk for all algorithms. + for (int i = 0; i < chunkDigests.size(); i++) { + ChunkDigests chunkDigest = chunkDigests.get(i); + int actualDigestSize = messageDigests.get(i).digest( + chunkDigest.concatOfDigestsOfChunks, + chunkDigest.getOffset(chunk.chunkIndex), + chunkDigest.digestOutputSize); + if (actualDigestSize != chunkDigest.digestOutputSize) { + throw new RuntimeException( + "Unexpected output size of " + chunkDigest.algorithm + + " digest: " + actualDigestSize); + } + } + } + } catch (IOException | DigestException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Thread-safe 1MB DataSource chunk supplier. When bounds are met in a + * supplied {@link DataSource}, the data from the next {@link DataSource} + * are NOT concatenated. Only the next call to get() will fetch from the + * next {@link DataSource} in the input {@link DataSource} array. + */ + private static class ChunkSupplier implements Supplier { + private final DataSource[] dataSources; + private final int[] chunkCounts; + private final int totalChunkCount; + private final AtomicInteger nextIndex; + + private ChunkSupplier(DataSource[] dataSources) { + this.dataSources = dataSources; + chunkCounts = new int[dataSources.length]; + int totalChunkCount = 0; + for (int i = 0; i < dataSources.length; i++) { + long chunkCount = getChunkCount(dataSources[i].size(), + CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + if (chunkCount > Integer.MAX_VALUE) { + throw new RuntimeException( + String.format( + "Number of chunks in dataSource[%d] is greater than max int.", + i)); + } + chunkCounts[i] = (int)chunkCount; + totalChunkCount += chunkCount; + } + this.totalChunkCount = totalChunkCount; + nextIndex = new AtomicInteger(0); + } + + /** + * We map an integer index to the termination-adjusted dataSources 1MB chunks. + * Note that {@link Chunk}s could be less than 1MB, namely the last 1MB-aligned + * blocks in each input {@link DataSource} (unless the DataSource itself is + * 1MB-aligned). + */ + @Override + public ChunkSupplier.Chunk get() { + int index = nextIndex.getAndIncrement(); + if (index < 0 || index >= totalChunkCount) { + return null; + } + + int dataSourceIndex = 0; + int dataSourceChunkOffset = index; + for (; dataSourceIndex < dataSources.length; dataSourceIndex++) { + if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) { + break; + } + dataSourceChunkOffset -= chunkCounts[dataSourceIndex]; + } + + long remainingSize = Math.min( + dataSources[dataSourceIndex].size() - + dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, + CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + // Note that slicing may involve its own locking. We may wish to reimplement the + // underlying mechanism to get rid of that lock (e.g. ByteBufferDataSource should + // probably get reimplemented to a delegate model, such that grabbing a slice + // doesn't incur a lock). + return new Chunk( + dataSources[dataSourceIndex].slice( + dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, + remainingSize), + index); + } + + static class Chunk { + private final int chunkIndex; + private final DataSource dataSource; + + private Chunk(DataSource parentSource, int chunkIndex) { + this.chunkIndex = chunkIndex; + dataSource = parentSource; + } + } + } + + private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir, + DataSource eocd, Map outputContentDigests) + throws IOException, NoSuchAlgorithmException { + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint8[32] Merkle tree root hash of SHA-256 + // * @+32 bytes int64 Length of source data + int backBufferSize = + ContentDigestAlgorithm.VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes() + + Long.SIZE / Byte.SIZE; + ByteBuffer encoded = ByteBuffer.allocate(backBufferSize); + encoded.order(ByteOrder.LITTLE_ENDIAN); + + // Use 0s as salt for now. This also needs to be consistent in the fsverify header for + // kernel to use. + VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8]); + byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir, eocd); + encoded.put(rootHash); + encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size()); + + outputContentDigests.put(ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, encoded.array()); + } + + private static long getChunkCount(long inputSize, long chunkSize) { + return (inputSize + chunkSize - 1) / chunkSize; + } + + private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) { + result[offset] = (byte) (value & 0xff); + result[offset + 1] = (byte) ((value >> 8) & 0xff); + result[offset + 2] = (byte) ((value >> 16) & 0xff); + result[offset + 3] = (byte) ((value >> 24) & 0xff); + } + + public static byte[] encodePublicKey(PublicKey publicKey) + throws InvalidKeyException, NoSuchAlgorithmException { + byte[] encodedPublicKey = null; + if ("X.509".equals(publicKey.getFormat())) { + encodedPublicKey = publicKey.getEncoded(); + } + if (encodedPublicKey == null) { + try { + encodedPublicKey = + KeyFactory.getInstance(publicKey.getAlgorithm()) + .getKeySpec(publicKey, X509EncodedKeySpec.class) + .getEncoded(); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to obtain X.509 encoded form of public key " + publicKey + + " of class " + publicKey.getClass().getName(), + e); + } + } + if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) { + throw new InvalidKeyException( + "Failed to obtain X.509 encoded form of public key " + publicKey + + " of class " + publicKey.getClass().getName()); + } + return encodedPublicKey; + } + + public static List encodeCertificates(List certificates) + throws CertificateEncodingException { + List result = new ArrayList<>(certificates.size()); + for (X509Certificate certificate : certificates) { + result.add(certificate.getEncoded()); + } + return result; + } + + public static byte[] encodeAsLengthPrefixedElement(byte[] bytes) { + byte[][] adapterBytes = new byte[1][]; + adapterBytes[0] = bytes; + return encodeAsSequenceOfLengthPrefixedElements(adapterBytes); + } + + public static byte[] encodeAsSequenceOfLengthPrefixedElements(List sequence) { + return encodeAsSequenceOfLengthPrefixedElements( + sequence.toArray(new byte[sequence.size()][])); + } + + public static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) { + int payloadSize = 0; + for (byte[] element : sequence) { + payloadSize += 4 + element.length; + } + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + for (byte[] element : sequence) { + result.putInt(element.length); + result.put(element); + } + return result.array(); + } + + public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + List> sequence) { + int resultSize = 0; + for (Pair element : sequence) { + resultSize += 12 + element.getSecond().length; + } + ByteBuffer result = ByteBuffer.allocate(resultSize); + result.order(ByteOrder.LITTLE_ENDIAN); + for (Pair 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(); + } + + /** + * 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, ApkUtils.ZipSections zipSections, int blockId, Result result) + throws IOException, SignatureNotFoundException { + // Find the APK Signing Block. + DataSource apkSigningBlock; + long apkSigningBlockOffset; + try { + ApkUtils.ApkSigningBlock apkSigningBlockInfo = + ApkUtils.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, result); + return new SignatureInfo( + apkSignatureSchemeBlock, + apkSigningBlockOffset, + zipSections.getZipCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectory()); + } + + /** + * Generates a new DataSource representing the APK contents before the Central Directory with + * padding, if padding is requested. If the existing data entries before the Central Directory + * are already aligned, or no padding is requested, the original DataSource is used. This + * padding is used to allow for verity-based APK verification. + * + * @return {@code Pair} containing the potentially new {@code DataSource} and the amount of + * padding used. + */ + public static Pair generateApkSigningBlockPadding( + DataSource beforeCentralDir, + boolean apkSigningBlockPaddingSupported) { + + // Ensure APK Signing Block starts from page boundary. + int padSizeBeforeSigningBlock = 0; + if (apkSigningBlockPaddingSupported && + (beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) { + padSizeBeforeSigningBlock = (int) ( + ANDROID_COMMON_PAGE_ALIGNMENT_BYTES - + beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES); + beforeCentralDir = new ChainedDataSource( + beforeCentralDir, + DataSources.asDataSource( + ByteBuffer.allocate(padSizeBeforeSigningBlock))); + } + return Pair.of(beforeCentralDir, padSizeBeforeSigningBlock); + } + + public static DataSource copyWithModifiedCDOffset( + DataSource beforeCentralDir, DataSource eocd) throws IOException { + + // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory + // offset field is treated as pointing to the offset at which the APK Signing Block will + // start. + long centralDirOffsetForDigesting = beforeCentralDir.size(); + ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size()); + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + eocd.copyTo(0, (int) eocd.size(), eocdBuf); + eocdBuf.flip(); + ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting); + return DataSources.asDataSource(eocdBuf); + } + + public static byte[] generateApkSigningBlock( + List> apkSignatureSchemeBlockPairs) { + // FORMAT: + // uint64: size (excluding this field) + // repeated ID-value pairs: + // uint64: size (excluding this field) + // uint32: ID + // (size - 4) bytes: value + // (extra dummy ID-value for padding to make block size a multiple of 4096 bytes) + // uint64: size (same as the one above) + // uint128: magic + + int blocksSize = 0; + for (Pair schemeBlockPair : apkSignatureSchemeBlockPairs) { + blocksSize += 8 + 4 + schemeBlockPair.getFirst().length; // size + id + value + } + + int resultSize = + 8 // size + + blocksSize + + 8 // size + + 16 // magic + ; + ByteBuffer paddingPair = null; + if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) { + int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES - + (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES); + if (padding < 12) { // minimum size of an ID-value pair + padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES; + } + paddingPair = ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN); + paddingPair.putLong(padding - 8); + paddingPair.putInt(VERITY_PADDING_BLOCK_ID); + paddingPair.rewind(); + resultSize += padding; + } + + ByteBuffer result = ByteBuffer.allocate(resultSize); + result.order(ByteOrder.LITTLE_ENDIAN); + long blockSizeFieldValue = resultSize - 8L; + result.putLong(blockSizeFieldValue); + + + for (Pair schemeBlockPair : apkSignatureSchemeBlockPairs) { + byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst(); + int apkSignatureSchemeId = schemeBlockPair.getSecond(); + long pairSizeFieldValue = 4L + apkSignatureSchemeBlock.length; + result.putLong(pairSizeFieldValue); + result.putInt(apkSignatureSchemeId); + result.put(apkSignatureSchemeBlock); + } + + if (paddingPair != null) { + result.put(paddingPair); + } + + result.putLong(blockSizeFieldValue); + result.put(APK_SIGNING_BLOCK_MAGIC); + + return result.array(); + } + + /** + * Computes the digests of the given APK components according to the algorithms specified in the + * given SignerConfigs. + * + * @param signerConfigs signer configurations, one for each signer At least one signer config + * must be provided. + * + * @throws IOException if an I/O error occurs + * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is + * missing + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static Pair, Map> + computeContentDigests( + RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List signerConfigs) + throws IOException, NoSuchAlgorithmException, SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException( + "No signer configs provided. At least one is required"); + } + + // Figure out which digest(s) to use for APK contents. + Set contentDigestAlgorithms = new HashSet<>(1); + for (SignerConfig signerConfig : signerConfigs) { + for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { + contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm()); + } + } + + // Compute digests of APK contents. + Map contentDigests; // digest algorithm ID -> digest + try { + contentDigests = + computeContentDigests( + executor, + contentDigestAlgorithms, + beforeCentralDir, + centralDir, + eocd); + } catch (IOException e) { + throw new IOException("Failed to read APK being signed", e); + } catch (DigestException e) { + throw new SignatureException("Failed to compute digests of APK", e); + } + + // Sign the digests and wrap the signatures and signer info into an APK Signing Block. + return Pair.of(signerConfigs, contentDigests); + } + + /** + * 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. + * + *

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 NoSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static List getSignaturesToVerify( + List signatures, int minSdkVersion, int maxSdkVersion) + throws NoSupportedSignaturesException { + // 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 bestSigAlgorithmOnSdkVersion = new HashMap<>(); + int minProvidedSignaturesVersion = Integer.MAX_VALUE; + for (SupportedSignature sig : signatures) { + SignatureAlgorithm sigAlgorithm = sig.algorithm; + int sigMinSdkVersion = sigAlgorithm.getMinSdkVersion(); + if (sigMinSdkVersion > maxSdkVersion) { + continue; + } + if (sigMinSdkVersion < minProvidedSignaturesVersion) { + minProvidedSignaturesVersion = sigMinSdkVersion; + } + + SupportedSignature 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 NoSupportedSignaturesException( + "Minimum provided signature version " + minProvidedSignaturesVersion + + " < minSdkVersion " + minSdkVersion); + } + if (bestSigAlgorithmOnSdkVersion.isEmpty()) { + throw new NoSupportedSignaturesException("No supported signature"); + } + return bestSigAlgorithmOnSdkVersion.values().stream() + .sorted((sig1, sig2) -> Integer.compare( + sig1.algorithm.getId(), sig2.algorithm.getId())) + .collect(Collectors.toList()); + } + + public static class NoSupportedSignaturesException extends Exception { + private static final long serialVersionUID = 1L; + + public NoSupportedSignaturesException(String message) { + super(message); + } + } + + public static class SignatureNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + public SignatureNotFoundException(String message) { + super(message); + } + + public SignatureNotFoundException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * uses the SignatureAlgorithms in the provided signerConfig to sign the provided data + * + * @return list of signature algorithm IDs and their corresponding signatures over the data. + */ + public static List> generateSignaturesOverData( + SignerConfig signerConfig, byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { + List> signatures = + new ArrayList<>(signerConfig.signatureAlgorithms.size()); + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { + Pair sigAlgAndParams = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams(); + String jcaSignatureAlgorithm = sigAlgAndParams.getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond(); + byte[] signatureBytes; + try { + Signature signature = Signature.getInstance(jcaSignatureAlgorithm); + signature.initSign(signerConfig.privateKey); + if (jcaSignatureAlgorithmParams != null) { + signature.setParameter(jcaSignatureAlgorithmParams); + } + signature.update(data); + signatureBytes = signature.sign(); + } catch (InvalidKeyException e) { + throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e); + } catch (InvalidAlgorithmParameterException | SignatureException e) { + throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e); + } + + try { + Signature signature = Signature.getInstance(jcaSignatureAlgorithm); + signature.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + signature.setParameter(jcaSignatureAlgorithmParams); + } + signature.update(data); + if (!signature.verify(signatureBytes)) { + throw new SignatureException("Failed to verify generated " + + jcaSignatureAlgorithm + + " signature using public key from certificate"); + } + } catch (InvalidKeyException e) { + throw new InvalidKeyException( + "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" + + " public key from certificate", e); + } catch (InvalidAlgorithmParameterException | SignatureException e) { + throw new SignatureException( + "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" + + " public key from certificate", e); + } + + signatures.add(Pair.of(signatureAlgorithm.getId(), signatureBytes)); + } + return signatures; + } + + /** + * Signer configuration. + */ + public static class SignerConfig { + /** Private key. */ + public PrivateKey privateKey; + + /** + * Certificates, with the first certificate containing the public key corresponding to + * {@link #privateKey}. + */ + public List certificates; + + /** + * List of signature algorithms with which to sign. + */ + public List signatureAlgorithms; + + public int minSdkVersion; + public int maxSdkVersion; + public SigningCertificateLineage mSigningCertificateLineage; + } + + public static class Result { + public final int signatureSchemeVersion; + + /** Whether the APK's APK Signature Scheme signature verifies. */ + public boolean verified; + + public final List signers = new ArrayList<>(); + public SigningCertificateLineage signingCertificateLineage = null; + private final List mWarnings = new ArrayList<>(); + private final List mErrors = new ArrayList<>(); + + public Result(int signatureSchemeVersion) { + this.signatureSchemeVersion = signatureSchemeVersion; + } + + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + if (!signers.isEmpty()) { + for (SignerInfo signer : signers) { + if (signer.containsErrors()) { + return true; + } + } + } + return false; + } + + public void addError(ApkVerifier.Issue msg, Object... parameters) { + mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters)); + } + + public void addWarning(ApkVerifier.Issue msg, Object... parameters) { + mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters)); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + + public static class SignerInfo { + public int index; + public List certs = new ArrayList<>(); + public List contentDigests = new ArrayList<>(); + public Map verifiedContentDigests = new HashMap<>(); + public List signatures = new ArrayList<>(); + public Map verifiedSignatures = new HashMap<>(); + public List additionalAttributes = new ArrayList<>(); + public byte[] signedData; + public int minSdkVersion; + public int maxSdkVersion; + public SigningCertificateLineage signingCertificateLineage; + + private final List mWarnings = new ArrayList<>(); + private final List mErrors = new ArrayList<>(); + + public void addError(ApkVerifier.Issue msg, Object... parameters) { + mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters)); + } + + public void addWarning(ApkVerifier.Issue msg, Object... parameters) { + mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters)); + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + + public static class ContentDigest { + private final int mSignatureAlgorithmId; + private final byte[] mValue; + + public ContentDigest(int signatureAlgorithmId, byte[] value) { + mSignatureAlgorithmId = signatureAlgorithmId; + mValue = value; + } + + public int getSignatureAlgorithmId() { + return mSignatureAlgorithmId; + } + + public byte[] getValue() { + return mValue; + } + } + + public static class Signature { + private final int mAlgorithmId; + private final byte[] mValue; + + public Signature(int algorithmId, byte[] value) { + mAlgorithmId = algorithmId; + mValue = value; + } + + public int getAlgorithmId() { + return mAlgorithmId; + } + + public byte[] getValue() { + return mValue; + } + } + + public static class AdditionalAttribute { + private final int mId; + private final byte[] mValue; + + public AdditionalAttribute(int id, byte[] value) { + mId = id; + mValue = value.clone(); + } + + public int getId() { + return mId; + } + + public byte[] getValue() { + return mValue.clone(); + } + } + } + } + + public static class SupportedSignature { + public final SignatureAlgorithm algorithm; + public final byte[] signature; + + public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) { + this.algorithm = algorithm; + this.signature = signature; + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java b/app/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java new file mode 100644 index 0000000..b222474 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java @@ -0,0 +1,55 @@ +/* + * 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("SHA-256", 256 / 8), + + /** SHA2-512 over 1 MB chunks. */ + CHUNKED_SHA512("SHA-512", 512 / 8), + + /** SHA2-256 over 4 KB chunks for APK verity. */ + VERITY_CHUNKED_SHA256("SHA-256", 256 / 8); + + private final String mJcaMessageDigestAlgorithm; + private final int mChunkDigestOutputSizeBytes; + + private ContentDigestAlgorithm( + String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) { + mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm; + mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes; + } + + /** + * 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java b/app/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java new file mode 100644 index 0000000..0db8cb8 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java @@ -0,0 +1,193 @@ +/* + * 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), + + /** + * 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), + + /** 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), + + /** 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), + + /** 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), + + /** 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), + + /** 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), + + /** + * 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), + + /** + * 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), + + /** + * 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); + + private final int mId; + private final String mJcaKeyAlgorithm; + private final ContentDigestAlgorithm mContentDigestAlgorithm; + private final Pair mJcaSignatureAlgAndParams; + private final int mMinSdkVersion; + + SignatureAlgorithm(int id, + ContentDigestAlgorithm contentDigestAlgorithm, + String jcaKeyAlgorithm, + Pair jcaSignatureAlgAndParams, + int minSdkVersion) { + mId = id; + mContentDigestAlgorithm = contentDigestAlgorithm; + mJcaKeyAlgorithm = jcaKeyAlgorithm; + mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams; + mMinSdkVersion = minSdkVersion; + } + + /** + * 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 getJcaSignatureAlgorithmAndParams() { + return mJcaSignatureAlgAndParams; + } + + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + public static SignatureAlgorithm findById(int id) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { + if (alg.getId() == id) { + return alg; + } + } + + return null; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java b/app/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java new file mode 100644 index 0000000..5e26327 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java @@ -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; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java b/app/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java new file mode 100644 index 0000000..51b9810 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java @@ -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 BY_STRENGTH_COMPARATOR = new StrengthComparator(); + + private static class StrengthComparator implements Comparator { + @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); + } + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java b/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java new file mode 100644 index 0000000..f900211 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java @@ -0,0 +1,688 @@ +/* + * 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 com.android.apksig.apk.ApkFormatException; +import com.android.apksig.internal.asn1.Asn1DerEncoder; +import com.android.apksig.internal.asn1.Asn1EncodingException; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.ber.BerEncoding; +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.pkcs7.ContentInfo; +import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo; +import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber; +import com.android.apksig.internal.pkcs7.Pkcs7Constants; +import com.android.apksig.internal.pkcs7.SignedData; +import com.android.apksig.internal.pkcs7.SignerIdentifier; +import com.android.apksig.internal.pkcs7.SignerInfo; +import com.android.apksig.internal.util.Pair; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +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.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; +import javax.security.auth.x500.X500Principal; + +/** + * APK signer which uses JAR signing (aka v1 signing scheme). + * + * @see Signed JAR File + */ +public abstract class V1SchemeSigner { + + public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; + + 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"; + + static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = "X-Android-APK-Signed"; + private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME = + new Attributes.Name(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 certificates; + + /** + * Digest algorithm used for the signature. + */ + public DigestAlgorithm signatureDigestAlgorithm; + } + + /** 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)) { + // 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> sign( + List signerConfigs, + DigestAlgorithm jarEntryDigestAlgorithm, + Map jarEntryDigests, + List 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> signManifest( + List signerConfigs, + DigestAlgorithm digestAlgorithm, + List 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> 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(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 getOutputEntryNames(List signerConfigs) { + Set 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(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 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 sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet()); + Collections.sort(sortedEntryNames); + SortedMap 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 individualSectionsContents; + public Attributes mainSectionAttributes; + } + + private static byte[] generateSignatureFile( + List 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 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(); + } + + /** ASN.1 DER-encoded {@code NULL}. */ + private static final Asn1OpaqueObject ASN1_DER_NULL = + new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0}); + + /** + * 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 signerCerts = signerConfig.certificates; + X509Certificate signingCert = signerCerts.get(0); + PublicKey publicKey = signingCert.getPublicKey(); + DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm; + Pair signatureAlgs = + getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm); + 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); + } + + // Wrap the signature into the JAR signature block which is created according to CMS PKCS #7 + // RFC 5652. + // The high-level simplified structure is as follows: + // ContentInfo + // digestAlgorithm + // SignedData + // bag of certificates + // SignerInfo + // signing cert issuer and serial number (for locating the cert in the above bag) + // digestAlgorithm + // signatureAlgorithm + // signature + try { + SignerInfo signerInfo = new SignerInfo(); + signerInfo.version = 1; + X500Principal signerCertIssuer = signingCert.getIssuerX500Principal(); + signerInfo.sid = + new SignerIdentifier( + new IssuerAndSerialNumber( + new Asn1OpaqueObject(signerCertIssuer.getEncoded()), + signingCert.getSerialNumber())); + AlgorithmIdentifier digestAlgorithmId = + getSignerInfoDigestAlgorithmOid(digestAlgorithm); + AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond(); + signerInfo.digestAlgorithm = digestAlgorithmId; + signerInfo.signatureAlgorithm = signatureAlgorithmId; + signerInfo.signature = ByteBuffer.wrap(signatureBytes); + + SignedData signedData = new SignedData(); + signedData.certificates = new ArrayList<>(signerCerts.size()); + for (X509Certificate cert : signerCerts) { + signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded())); + } + signedData.version = 1; + signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId); + signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA); + signedData.signerInfos = Collections.singletonList(signerInfo); + + ContentInfo contentInfo = new ContentInfo(); + contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA; + contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData)); + return Asn1DerEncoder.encode(contentInfo); + } catch (Asn1EncodingException e) { + throw new SignatureException("Failed to encode signature block", e); + } + } + + /** + * Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest + * algorithm. + */ + private static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid( + DigestAlgorithm digestAlgorithm) { + switch (digestAlgorithm) { + case SHA1: + return new AlgorithmIdentifier( + V1SchemeVerifier.Signer.OID_DIGEST_SHA1, ASN1_DER_NULL); + case SHA256: + return new AlgorithmIdentifier( + V1SchemeVerifier.Signer.OID_DIGEST_SHA256, ASN1_DER_NULL); + default: + throw new RuntimeException("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. + */ + private static Pair getSignerInfoSignatureAlgorithm( + PublicKey publicKey, DigestAlgorithm digestAlgorithm) 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)) { + return Pair.of( + jcaDigestPrefixForSigAlg + "withRSA", + new AlgorithmIdentifier(V1SchemeVerifier.Signer.OID_SIG_RSA, ASN1_DER_NULL)); + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + AlgorithmIdentifier sigAlgId; + switch (digestAlgorithm) { + case SHA1: + sigAlgId = + new AlgorithmIdentifier( + V1SchemeVerifier.Signer.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( + V1SchemeVerifier.Signer.OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL); + break; + default: + throw new IllegalArgumentException( + "Unexpected digest algorithm: " + digestAlgorithm); + } + return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId); + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + return Pair.of( + jcaDigestPrefixForSigAlg + "withECDSA", + new AlgorithmIdentifier( + V1SchemeVerifier.Signer.OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL)); + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } + + 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); + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java new file mode 100644 index 0000000..47d5b01 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java @@ -0,0 +1,2099 @@ +/* + * 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 com.android.apksig.ApkVerifier.Issue; +import com.android.apksig.ApkVerifier.IssueWithParams; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.asn1.Asn1BerParser; +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1DecodingException; +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.jar.ManifestParser; +import com.android.apksig.internal.pkcs7.Attribute; +import com.android.apksig.internal.pkcs7.ContentInfo; +import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber; +import com.android.apksig.internal.pkcs7.Pkcs7Constants; +import com.android.apksig.internal.pkcs7.Pkcs7DecodingException; +import com.android.apksig.internal.pkcs7.SignedData; +import com.android.apksig.internal.pkcs7.SignerIdentifier; +import com.android.apksig.internal.pkcs7.SignerInfo; +import com.android.apksig.internal.util.AndroidSdkVersion; +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.internal.util.InclusiveIntRange; +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.util.DataSinks; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Base64.Decoder; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.jar.Attributes; + +import javax.security.auth.x500.X500Principal; + +/** + * APK verifier which uses JAR signing (aka v1 signing scheme). + * + * @see Signed JAR File + */ +public abstract class V1SchemeVerifier { + + private static final String MANIFEST_ENTRY_NAME = V1SchemeSigner.MANIFEST_ENTRY_NAME; + + private V1SchemeVerifier() {} + + /** + * Verifies the provided APK's JAR signatures and returns the result of verification. APK is + * considered verified only if {@link Result#verified} is {@code true}. If verification fails, + * the result will contain errors -- see {@link Result#getErrors()}. + * + *

Verification succeeds iff the APK's JAR 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 IOException if an I/O error occurs when reading the APK + * @throws NoSuchAlgorithmException if the APK's JAR signatures cannot be verified because a + * required cryptographic algorithm implementation is missing + */ + public static Result verify( + DataSource apk, + ApkUtils.ZipSections apkSections, + Map supportedApkSigSchemeNames, + Set foundApkSigSchemeIds, + int minSdkVersion, + int maxSdkVersion) throws IOException, ApkFormatException, NoSuchAlgorithmException { + if (minSdkVersion > maxSdkVersion) { + throw new IllegalArgumentException( + "minSdkVersion (" + minSdkVersion + ") > maxSdkVersion (" + maxSdkVersion + + ")"); + } + + Result result = new Result(); + + // Parse the ZIP Central Directory and check that there are no entries with duplicate names. + List cdRecords = parseZipCentralDirectory(apk, apkSections); + Set cdEntryNames = checkForDuplicateEntries(cdRecords, result); + if (result.containsErrors()) { + return result; + } + + // Verify JAR signature(s). + Signers.verify( + apk, + apkSections.getZipCentralDirectoryOffset(), + cdRecords, + cdEntryNames, + supportedApkSigSchemeNames, + foundApkSigSchemeIds, + minSdkVersion, + maxSdkVersion, + result); + + return result; + } + + /** + * Returns the set of entry names and reports any duplicate entry names in the {@code result} + * as errors. + */ + private static Set checkForDuplicateEntries( + List cdRecords, Result result) { + Set cdEntryNames = new HashSet<>(cdRecords.size()); + Set duplicateCdEntryNames = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + String entryName = cdRecord.getName(); + if (!cdEntryNames.add(entryName)) { + // This is an error. Report this once per duplicate name. + if (duplicateCdEntryNames == null) { + duplicateCdEntryNames = new HashSet<>(); + } + if (duplicateCdEntryNames.add(entryName)) { + result.addError(Issue.JAR_SIG_DUPLICATE_ZIP_ENTRY, entryName); + } + } + } + return cdEntryNames; + } + + /** + * Parses raw representation of MANIFEST.MF file into a pair of main entry manifest section + * representation and a mapping between entry name and its manifest section representation. + * + * @param manifestBytes raw representation of Manifest.MF + * @param cdEntryNames expected set of entry names + * @param result object to keep track of errors that happened during the parsing + * @return a pair of main entry manifest section representation and a mapping between entry name + * and its manifest section representation + */ + public static Pair> parseManifest( + byte[] manifestBytes, Set cdEntryNames, Result result) { + ManifestParser manifest = new ManifestParser(manifestBytes); + ManifestParser.Section manifestMainSection = manifest.readSection(); + List manifestIndividualSections = manifest.readAllSections(); + Map entryNameToManifestSection = + new HashMap<>(manifestIndividualSections.size()); + int manifestSectionNumber = 0; + for (ManifestParser.Section manifestSection : manifestIndividualSections) { + manifestSectionNumber++; + String entryName = manifestSection.getName(); + if (entryName == null) { + result.addError(Issue.JAR_SIG_UNNNAMED_MANIFEST_SECTION, manifestSectionNumber); + continue; + } + if (entryNameToManifestSection.put(entryName, manifestSection) != null) { + result.addError(Issue.JAR_SIG_DUPLICATE_MANIFEST_SECTION, entryName); + continue; + } + if (!cdEntryNames.contains(entryName)) { + result.addError( + Issue.JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST, entryName); + continue; + } + } + return Pair.of(manifestMainSection, entryNameToManifestSection); + } + + /** + * All JAR signers of an APK. + */ + private static class Signers { + + /** + * Verifies JAR signatures of the provided APK and populates the provided result container + * with errors, warnings, and information about signers. The APK is considered verified if + * the {@link Result#verified} is {@code true}. + */ + private static void verify( + DataSource apk, + long cdStartOffset, + List cdRecords, + Set cdEntryNames, + Map supportedApkSigSchemeNames, + Set foundApkSigSchemeIds, + int minSdkVersion, + int maxSdkVersion, + Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException { + + // Find JAR manifest and signature block files. + CentralDirectoryRecord manifestEntry = null; + Map sigFileEntries = new HashMap<>(1); + List sigBlockEntries = new ArrayList<>(1); + for (CentralDirectoryRecord cdRecord : cdRecords) { + String entryName = cdRecord.getName(); + if (!entryName.startsWith("META-INF/")) { + continue; + } + if ((manifestEntry == null) && (MANIFEST_ENTRY_NAME.equals(entryName))) { + manifestEntry = cdRecord; + continue; + } + if (entryName.endsWith(".SF")) { + sigFileEntries.put(entryName, cdRecord); + continue; + } + if ((entryName.endsWith(".RSA")) + || (entryName.endsWith(".DSA")) + || (entryName.endsWith(".EC"))) { + sigBlockEntries.add(cdRecord); + continue; + } + } + if (manifestEntry == null) { + result.addError(Issue.JAR_SIG_NO_MANIFEST); + return; + } + + // Parse the JAR manifest and check that all JAR entries it references exist in the APK. + byte[] manifestBytes; + try { + manifestBytes = + LocalFileRecord.getUncompressedData(apk, manifestEntry, cdStartOffset); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed ZIP entry: " + manifestEntry.getName(), e); + } + + Pair> manifestSections = + parseManifest(manifestBytes, cdEntryNames, result); + + if (result.containsErrors()) { + return; + } + + ManifestParser.Section manifestMainSection = manifestSections.getFirst(); + Map entryNameToManifestSection = + manifestSections.getSecond(); + + // STATE OF AFFAIRS: + // * All JAR entries listed in JAR manifest are present in the APK. + + // Identify signers + List signers = new ArrayList<>(sigBlockEntries.size()); + for (CentralDirectoryRecord sigBlockEntry : sigBlockEntries) { + String sigBlockEntryName = sigBlockEntry.getName(); + int extensionDelimiterIndex = sigBlockEntryName.lastIndexOf('.'); + if (extensionDelimiterIndex == -1) { + throw new RuntimeException( + "Signature block file name does not contain extension: " + + sigBlockEntryName); + } + String sigFileEntryName = + sigBlockEntryName.substring(0, extensionDelimiterIndex) + ".SF"; + CentralDirectoryRecord sigFileEntry = sigFileEntries.get(sigFileEntryName); + if (sigFileEntry == null) { + result.addWarning( + Issue.JAR_SIG_MISSING_FILE, sigBlockEntryName, sigFileEntryName); + continue; + } + String signerName = sigBlockEntryName.substring("META-INF/".length()); + Result.SignerInfo signerInfo = + new Result.SignerInfo( + signerName, sigBlockEntryName, sigFileEntry.getName()); + Signer signer = new Signer(signerName, sigBlockEntry, sigFileEntry, signerInfo); + signers.add(signer); + } + if (signers.isEmpty()) { + result.addError(Issue.JAR_SIG_NO_SIGNATURES); + return; + } + + // Verify each signer's signature block file .(RSA|DSA|EC) against the corresponding + // signature file .SF. Any error encountered for any signer terminates verification, to + // mimic Android's behavior. + for (Signer signer : signers) { + signer.verifySigBlockAgainstSigFile( + apk, cdStartOffset, minSdkVersion, maxSdkVersion); + if (signer.getResult().containsErrors()) { + result.signers.add(signer.getResult()); + } + } + if (result.containsErrors()) { + return; + } + // STATE OF AFFAIRS: + // * All JAR entries listed in JAR manifest are present in the APK. + // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). + + // Verify each signer's signature file (.SF) against the JAR manifest. + List remainingSigners = new ArrayList<>(signers.size()); + for (Signer signer : signers) { + signer.verifySigFileAgainstManifest( + manifestBytes, + manifestMainSection, + entryNameToManifestSection, + supportedApkSigSchemeNames, + foundApkSigSchemeIds, + minSdkVersion, + maxSdkVersion); + if (signer.isIgnored()) { + result.ignoredSigners.add(signer.getResult()); + } else { + if (signer.getResult().containsErrors()) { + result.signers.add(signer.getResult()); + } else { + remainingSigners.add(signer); + } + } + } + if (result.containsErrors()) { + return; + } + signers = remainingSigners; + if (signers.isEmpty()) { + result.addError(Issue.JAR_SIG_NO_SIGNATURES); + return; + } + // STATE OF AFFAIRS: + // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). + // * Contents of all JAR manifest sections listed in .SF files verify against .SF files. + // * All JAR entries listed in JAR manifest are present in the APK. + + // Verify data of JAR entries against JAR manifest and .SF files. On Android, an APK's + // JAR entry is considered signed by signers associated with an .SF file iff the entry + // is mentioned in the .SF file and the entry's digest(s) mentioned in the JAR manifest + // match theentry's uncompressed data. Android requires that all such JAR entries are + // signed by the same set of signers. This set may be smaller than the set of signers + // we've identified so far. + Set apkSigners = + verifyJarEntriesAgainstManifestAndSigners( + apk, + cdStartOffset, + cdRecords, + entryNameToManifestSection, + signers, + minSdkVersion, + maxSdkVersion, + result); + if (result.containsErrors()) { + return; + } + // STATE OF AFFAIRS: + // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). + // * Contents of all JAR manifest sections listed in .SF files verify against .SF files. + // * All JAR entries listed in JAR manifest are present in the APK. + // * All JAR entries present in the APK and supposed to be covered by JAR signature + // (i.e., reside outside of META-INF/) are covered by signatures from the same set + // of signers. + + // Report any JAR entries which aren't covered by signature. + Set signatureEntryNames = new HashSet<>(1 + result.signers.size() * 2); + signatureEntryNames.add(manifestEntry.getName()); + for (Signer signer : apkSigners) { + signatureEntryNames.add(signer.getSignatureBlockEntryName()); + signatureEntryNames.add(signer.getSignatureFileEntryName()); + } + for (CentralDirectoryRecord cdRecord : cdRecords) { + String entryName = cdRecord.getName(); + if ((entryName.startsWith("META-INF/")) + && (!entryName.endsWith("/")) + && (!signatureEntryNames.contains(entryName))) { + result.addWarning(Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY, entryName); + } + } + + // Reflect the sets of used signers and ignored signers in the result. + for (Signer signer : signers) { + if (apkSigners.contains(signer)) { + result.signers.add(signer.getResult()); + } else { + result.ignoredSigners.add(signer.getResult()); + } + } + + result.verified = true; + } + } + + static class Signer { + private final String mName; + private final Result.SignerInfo mResult; + private final CentralDirectoryRecord mSignatureFileEntry; + private final CentralDirectoryRecord mSignatureBlockEntry; + private boolean mIgnored; + + private byte[] mSigFileBytes; + private Set mSigFileEntryNames; + + private Signer( + String name, + CentralDirectoryRecord sigBlockEntry, + CentralDirectoryRecord sigFileEntry, + Result.SignerInfo result) { + mName = name; + mResult = result; + mSignatureBlockEntry = sigBlockEntry; + mSignatureFileEntry = sigFileEntry; + } + + public String getName() { + return mName; + } + + public String getSignatureFileEntryName() { + return mSignatureFileEntry.getName(); + } + + public String getSignatureBlockEntryName() { + return mSignatureBlockEntry.getName(); + } + + void setIgnored() { + mIgnored = true; + } + + public boolean isIgnored() { + return mIgnored; + } + + public Set getSigFileEntryNames() { + return mSigFileEntryNames; + } + + public Result.SignerInfo getResult() { + return mResult; + } + + public void verifySigBlockAgainstSigFile( + DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion) + throws IOException, ApkFormatException, NoSuchAlgorithmException { + // Obtain the signature block from the APK + byte[] sigBlockBytes; + try { + sigBlockBytes = + LocalFileRecord.getUncompressedData( + apk, mSignatureBlockEntry, cdStartOffset); + } catch (ZipFormatException e) { + throw new ApkFormatException( + "Malformed ZIP entry: " + mSignatureBlockEntry.getName(), e); + } + // Obtain the signature file from the APK + try { + mSigFileBytes = + LocalFileRecord.getUncompressedData( + apk, mSignatureFileEntry, cdStartOffset); + } catch (ZipFormatException e) { + throw new ApkFormatException( + "Malformed ZIP entry: " + mSignatureFileEntry.getName(), e); + } + + // Extract PKCS #7 SignedData from the signature block + SignedData signedData; + try { + ContentInfo contentInfo = + Asn1BerParser.parse(ByteBuffer.wrap(sigBlockBytes), ContentInfo.class); + if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) { + throw new Asn1DecodingException( + "Unsupported ContentInfo.contentType: " + contentInfo.contentType); + } + signedData = + Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class); + } catch (Asn1DecodingException e) { + e.printStackTrace(); + mResult.addError( + Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); + return; + } + + if (signedData.signerInfos.isEmpty()) { + mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName()); + return; + } + + // Find the first SignedData.SignerInfos element which verifies against the signature + // file + SignerInfo firstVerifiedSignerInfo = null; + X509Certificate firstVerifiedSignerInfoSigningCertificate = null; + // Prior to Android N, Android attempts to verify only the first SignerInfo. From N + // onwards, Android attempts to verify all SignerInfos and then picks the first verified + // SignerInfo. + List unverifiedSignerInfosToTry; + if (minSdkVersion < AndroidSdkVersion.N) { + unverifiedSignerInfosToTry = + Collections.singletonList(signedData.signerInfos.get(0)); + } else { + unverifiedSignerInfosToTry = signedData.signerInfos; + } + List signedDataCertificates = null; + for (SignerInfo unverifiedSignerInfo : unverifiedSignerInfosToTry) { + // Parse SignedData.certificates -- they are needed to verify SignerInfo + if (signedDataCertificates == null) { + try { + signedDataCertificates = parseCertificates(signedData.certificates); + } catch (CertificateException e) { + mResult.addError( + Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); + return; + } + } + + // Verify SignerInfo + X509Certificate signingCertificate; + try { + signingCertificate = + verifySignerInfoAgainstSigFile( + signedData, + signedDataCertificates, + unverifiedSignerInfo, + mSigFileBytes, + minSdkVersion, + maxSdkVersion); + if (mResult.containsErrors()) { + return; + } + if (signingCertificate != null) { + // SignerInfo verified + if (firstVerifiedSignerInfo == null) { + firstVerifiedSignerInfo = unverifiedSignerInfo; + firstVerifiedSignerInfoSigningCertificate = signingCertificate; + } + } + } catch (Pkcs7DecodingException e) { + mResult.addError( + Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); + return; + } catch (InvalidKeyException | SignatureException e) { + mResult.addError( + Issue.JAR_SIG_VERIFY_EXCEPTION, + mSignatureBlockEntry.getName(), + mSignatureFileEntry.getName(), + e); + return; + } + } + if (firstVerifiedSignerInfo == null) { + // No SignerInfo verified + mResult.addError( + Issue.JAR_SIG_DID_NOT_VERIFY, + mSignatureBlockEntry.getName(), + mSignatureFileEntry.getName()); + return; + } + // Verified + List signingCertChain = + getCertificateChain( + signedDataCertificates, firstVerifiedSignerInfoSigningCertificate); + mResult.certChain.clear(); + mResult.certChain.addAll(signingCertChain); + } + + /** + * Returns the signing certificate if the provided {@link SignerInfo} verifies against the + * contents of the provided signature file, or {@code null} if it does not verify. + */ + private X509Certificate verifySignerInfoAgainstSigFile( + SignedData signedData, + Collection signedDataCertificates, + SignerInfo signerInfo, + byte[] signatureFile, + int minSdkVersion, + int maxSdkVersion) + throws Pkcs7DecodingException, NoSuchAlgorithmException, + InvalidKeyException, SignatureException { + String digestAlgorithmOid = signerInfo.digestAlgorithm.algorithm; + String signatureAlgorithmOid = signerInfo.signatureAlgorithm.algorithm; + InclusiveIntRange desiredApiLevels = + InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion); + List apiLevelsWhereDigestAndSigAlgorithmSupported = + getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid); + List apiLevelsWhereDigestAlgorithmNotSupported = + desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported); + if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) { + String digestAlgorithmUserFriendly = + OidToUserFriendlyNameMapper.getUserFriendlyNameForOid( + digestAlgorithmOid); + if (digestAlgorithmUserFriendly == null) { + digestAlgorithmUserFriendly = digestAlgorithmOid; + } + String signatureAlgorithmUserFriendly = + OidToUserFriendlyNameMapper.getUserFriendlyNameForOid( + signatureAlgorithmOid); + if (signatureAlgorithmUserFriendly == null) { + signatureAlgorithmUserFriendly = signatureAlgorithmOid; + } + StringBuilder apiLevelsUserFriendly = new StringBuilder(); + for (InclusiveIntRange range : apiLevelsWhereDigestAlgorithmNotSupported) { + if (apiLevelsUserFriendly.length() > 0) { + apiLevelsUserFriendly.append(", "); + } + if (range.getMin() == range.getMax()) { + apiLevelsUserFriendly.append(String.valueOf(range.getMin())); + } else if (range.getMax() == Integer.MAX_VALUE) { + apiLevelsUserFriendly.append(range.getMin() + "+"); + } else { + apiLevelsUserFriendly.append(range.getMin() + "-" + range.getMax()); + } + } + mResult.addError( + Issue.JAR_SIG_UNSUPPORTED_SIG_ALG, + mSignatureBlockEntry.getName(), + digestAlgorithmOid, + signatureAlgorithmOid, + apiLevelsUserFriendly.toString(), + digestAlgorithmUserFriendly, + signatureAlgorithmUserFriendly); + return null; + } + + // From the bag of certs, obtain the certificate referenced by the SignerInfo, + // and verify the cryptographic signature in the SignerInfo against the certificate. + + // Locate the signing certificate referenced by the SignerInfo + X509Certificate signingCertificate = + findCertificate(signedDataCertificates, signerInfo.sid); + if (signingCertificate == null) { + throw new SignatureException( + "Signing certificate referenced in SignerInfo not found in" + + " SignedData"); + } + + // Check whether the signing certificate is acceptable. Android performs these + // checks explicitly, instead of delegating this to + // Signature.initVerify(Certificate). + if (signingCertificate.hasUnsupportedCriticalExtension()) { + throw new SignatureException( + "Signing certificate has unsupported critical extensions"); + } + boolean[] keyUsageExtension = signingCertificate.getKeyUsage(); + if (keyUsageExtension != null) { + boolean digitalSignature = + (keyUsageExtension.length >= 1) && (keyUsageExtension[0]); + boolean nonRepudiation = + (keyUsageExtension.length >= 2) && (keyUsageExtension[1]); + if ((!digitalSignature) && (!nonRepudiation)) { + throw new SignatureException( + "Signing certificate not authorized for use in digital signatures" + + ": keyUsage extension missing digitalSignature and" + + " nonRepudiation"); + } + } + + // Verify the cryptographic signature in SignerInfo against the certificate's + // public key + String jcaSignatureAlgorithm = + getJcaSignatureAlgorithm(digestAlgorithmOid, signatureAlgorithmOid); + Signature s = Signature.getInstance(jcaSignatureAlgorithm); + s.initVerify(signingCertificate.getPublicKey()); + if (signerInfo.signedAttrs != null) { + // Signed attributes present -- verify signature against the ASN.1 DER encoded form + // of signed attributes. This verifies integrity of the signature file because + // signed attributes must contain the digest of the signature file. + if (minSdkVersion < AndroidSdkVersion.KITKAT) { + // Prior to Android KitKat, APKs with signed attributes are unsafe: + // * The APK's contents are not protected by the JAR signature because the + // digest in signed attributes is not verified. This means an attacker can + // arbitrarily modify the APK without invalidating its signature. + // * Luckily, the signature over signed attributes was verified incorrectly + // (over the verbatim IMPLICIT [0] form rather than over re-encoded + // UNIVERSAL SET form) which means that JAR signatures which would verify on + // pre-KitKat Android and yet do not protect the APK from modification could + // be generated only by broken tools or on purpose by the entity signing the + // APK. + // + // We thus reject such unsafe APKs, even if they verify on platforms before + // KitKat. + throw new SignatureException( + "APKs with Signed Attributes broken on platforms with API Level < " + + AndroidSdkVersion.KITKAT); + } + try { + List signedAttributes = + Asn1BerParser.parseImplicitSetOf( + signerInfo.signedAttrs.getEncoded(), Attribute.class); + SignedAttributes signedAttrs = new SignedAttributes(signedAttributes); + if (maxSdkVersion >= AndroidSdkVersion.N) { + // Content Type attribute is checked only on Android N and newer + String contentType = + signedAttrs.getSingleObjectIdentifierValue( + Pkcs7Constants.OID_CONTENT_TYPE); + if (contentType == null) { + throw new SignatureException("No Content Type in signed attributes"); + } + if (!contentType.equals(signedData.encapContentInfo.contentType)) { + // Did not verify: Content type signed attribute does not match + // SignedData.encapContentInfo.eContentType. This fails verification of + // this SignerInfo but should not prevent verification of other + // SignerInfos. Hence, no exception is thrown. + return null; + } + } + byte[] expectedSignatureFileDigest = + signedAttrs.getSingleOctetStringValue( + Pkcs7Constants.OID_MESSAGE_DIGEST); + if (expectedSignatureFileDigest == null) { + throw new SignatureException("No content digest in signed attributes"); + } + byte[] actualSignatureFileDigest = + MessageDigest.getInstance( + getJcaDigestAlgorithm(digestAlgorithmOid)) + .digest(signatureFile); + if (!Arrays.equals( + expectedSignatureFileDigest, actualSignatureFileDigest)) { + // Skip verification: signature file digest in signed attributes does not + // match the signature file. This fails verification of + // this SignerInfo but should not prevent verification of other + // SignerInfos. Hence, no exception is thrown. + return null; + } + } catch (Asn1DecodingException e) { + throw new SignatureException("Failed to parse signed attributes", e); + } + // PKCS #7 requires that signature is over signed attributes re-encoded as + // ASN.1 DER. However, Android does not re-encode except for changing the + // first byte of encoded form from IMPLICIT [0] to UNIVERSAL SET. We do the + // same for maximum compatibility. + ByteBuffer signedAttrsOriginalEncoding = signerInfo.signedAttrs.getEncoded(); + s.update((byte) 0x31); // UNIVERSAL SET + signedAttrsOriginalEncoding.position(1); + s.update(signedAttrsOriginalEncoding); + } else { + // No signed attributes present -- verify signature against the contents of the + // signature file + s.update(signatureFile); + } + byte[] sigBytes = ByteBufferUtils.toByteArray(signerInfo.signature.slice()); + if (!s.verify(sigBytes)) { + // Cryptographic signature did not verify. This fails verification of this + // SignerInfo but should not prevent verification of other SignerInfos. Hence, no + // exception is thrown. + return null; + } + // Cryptographic signature verified + return signingCertificate; + } + + private static List parseCertificates( + List encodedCertificates) throws CertificateException { + if (encodedCertificates.isEmpty()) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(encodedCertificates.size()); + for (int i = 0; i < encodedCertificates.size(); i++) { + Asn1OpaqueObject encodedCertificate = encodedCertificates.get(i); + X509Certificate certificate; + byte[] encodedForm = ByteBufferUtils.toByteArray(encodedCertificate.getEncoded()); + try { + certificate = X509CertificateUtils.generateCertificate(encodedForm); + } catch (CertificateException e) { + throw new CertificateException("Failed to parse certificate #" + (i + 1), e); + } + // 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 and/or some implementations of + // X509Certificate.getEncoded() re-encode certificates. + certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedForm); + result.add(certificate); + } + return result; + } + + public static X509Certificate findCertificate( + Collection certs, SignerIdentifier id) { + for (X509Certificate cert : certs) { + if (isMatchingCerticicate(cert, id)) { + return cert; + } + } + return null; + } + + public static List getCertificateChain( + List certs, X509Certificate leaf) { + List unusedCerts = new ArrayList<>(certs); + List result = new ArrayList<>(1); + result.add(leaf); + unusedCerts.remove(leaf); + X509Certificate root = leaf; + while (!root.getSubjectDN().equals(root.getIssuerDN())) { + Principal targetDn = root.getIssuerDN(); + boolean issuerFound = false; + for (int i = 0; i < unusedCerts.size(); i++) { + X509Certificate unusedCert = unusedCerts.get(i); + if (targetDn.equals(unusedCert.getSubjectDN())) { + issuerFound = true; + unusedCerts.remove(i); + result.add(unusedCert); + root = unusedCert; + break; + } + } + if (!issuerFound) { + break; + } + } + return result; + } + + private static boolean isMatchingCerticicate(X509Certificate cert, SignerIdentifier id) { + if (id.issuerAndSerialNumber == null) { + // Android doesn't support any other means of identifying the signing certificate + return false; + } + IssuerAndSerialNumber issuerAndSerialNumber = id.issuerAndSerialNumber; + byte[] encodedIssuer = + ByteBufferUtils.toByteArray(issuerAndSerialNumber.issuer.getEncoded()); + X500Principal idIssuer = new X500Principal(encodedIssuer); + BigInteger idSerialNumber = issuerAndSerialNumber.certificateSerialNumber; + return idSerialNumber.equals(cert.getSerialNumber()) + && idIssuer.equals(cert.getIssuerX500Principal()); + } + + private static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5"; + static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26"; + private static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4"; + static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1"; + private static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2"; + private static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3"; + + static final String OID_SIG_RSA = "1.2.840.113549.1.1.1"; + private static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4"; + private static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5"; + private static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14"; + private static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11"; + private static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12"; + private static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13"; + + static final String OID_SIG_DSA = "1.2.840.10040.4.1"; + private static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3"; + private static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1"; + static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2"; + static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3"; + static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4"; + + static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1"; + private static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1"; + private static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1"; + private static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2"; + private static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3"; + private static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4"; + + private static final Map> SUPPORTED_SIG_ALG_OIDS = + new HashMap<>(); + { + 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)); + } + + private static void addSupportedSigAlg( + String digestAlgorithmOid, + String signatureAlgorithmOid, + InclusiveIntRange... supportedApiLevels) { + SUPPORTED_SIG_ALG_OIDS.put( + digestAlgorithmOid + "with" + signatureAlgorithmOid, + Arrays.asList(supportedApiLevels)); + } + + private List getSigAlgSupportedApiLevels( + String digestAlgorithmOid, + String signatureAlgorithmOid) { + List result = + SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid); + return (result != null) ? result : Collections.emptyList(); + } + + private static class OidToUserFriendlyNameMapper { + private OidToUserFriendlyNameMapper() {} + + private static final Map 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); + } + } + + private static final Map 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"); + } + + private 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; + } + + private static final Map 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 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 void verifySigFileAgainstManifest( + byte[] manifestBytes, + ManifestParser.Section manifestMainSection, + Map entryNameToManifestSection, + Map supportedApkSigSchemeNames, + Set foundApkSigSchemeIds, + int minSdkVersion, + int maxSdkVersion) throws NoSuchAlgorithmException { + // Inspect the main section of the .SF file. + ManifestParser sf = new ManifestParser(mSigFileBytes); + ManifestParser.Section sfMainSection = sf.readSection(); + if (sfMainSection.getAttributeValue(Attributes.Name.SIGNATURE_VERSION) == null) { + mResult.addError( + Issue.JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE, + mSignatureFileEntry.getName()); + setIgnored(); + return; + } + + if (maxSdkVersion >= AndroidSdkVersion.N) { + // Android N and newer rejects APKs whose .SF file says they were supposed to be + // signed with APK Signature Scheme v2 (or newer) and yet no such signature was + // found. + checkForStrippedApkSignatures( + sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds); + if (mResult.containsErrors()) { + return; + } + } + + boolean createdBySigntool = false; + String createdBy = sfMainSection.getAttributeValue("Created-By"); + if (createdBy != null) { + createdBySigntool = createdBy.indexOf("signtool") != -1; + } + boolean manifestDigestVerified = + verifyManifestDigest( + sfMainSection, + createdBySigntool, + manifestBytes, + minSdkVersion, + maxSdkVersion); + if (!createdBySigntool) { + verifyManifestMainSectionDigest( + sfMainSection, + manifestMainSection, + manifestBytes, + minSdkVersion, + maxSdkVersion); + } + if (mResult.containsErrors()) { + return; + } + + // Inspect per-entry sections of .SF file. Technically, if the digest of JAR manifest + // verifies, per-entry sections should be ignored. However, most Android platform + // implementations require that such sections exist. + List sfSections = sf.readAllSections(); + Set sfEntryNames = new HashSet<>(sfSections.size()); + int sfSectionNumber = 0; + for (ManifestParser.Section sfSection : sfSections) { + sfSectionNumber++; + String entryName = sfSection.getName(); + if (entryName == null) { + mResult.addError( + Issue.JAR_SIG_UNNNAMED_SIG_FILE_SECTION, + mSignatureFileEntry.getName(), + sfSectionNumber); + setIgnored(); + return; + } + if (!sfEntryNames.add(entryName)) { + mResult.addError( + Issue.JAR_SIG_DUPLICATE_SIG_FILE_SECTION, + mSignatureFileEntry.getName(), + entryName); + setIgnored(); + return; + } + if (manifestDigestVerified) { + // No need to verify this entry's corresponding JAR manifest entry because the + // JAR manifest verifies in full. + continue; + } + // Whole-file digest of JAR manifest hasn't been verified. Thus, we need to verify + // the digest of the JAR manifest section corresponding to this .SF section. + ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName); + if (manifestSection == null) { + mResult.addError( + Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE, + entryName, + mSignatureFileEntry.getName()); + setIgnored(); + continue; + } + verifyManifestIndividualSectionDigest( + sfSection, + createdBySigntool, + manifestSection, + manifestBytes, + minSdkVersion, + maxSdkVersion); + } + mSigFileEntryNames = sfEntryNames; + } + + + /** + * Returns {@code true} if the whole-file digest of the manifest against the main section of + * the .SF file. + */ + private boolean verifyManifestDigest( + ManifestParser.Section sfMainSection, + boolean createdBySigntool, + byte[] manifestBytes, + int minSdkVersion, + int maxSdkVersion) throws NoSuchAlgorithmException { + Collection expectedDigests = + getDigestsToVerify( + sfMainSection, + ((createdBySigntool) ? "-Digest" : "-Digest-Manifest"), + minSdkVersion, + maxSdkVersion); + boolean digestFound = !expectedDigests.isEmpty(); + if (!digestFound) { + mResult.addWarning( + Issue.JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE, + mSignatureFileEntry.getName()); + return false; + } + + boolean verified = true; + for (NamedDigest expectedDigest : expectedDigests) { + String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; + byte[] actual = digest(jcaDigestAlgorithm, manifestBytes); + byte[] expected = expectedDigest.digest; + if (!Arrays.equals(expected, actual)) { + mResult.addWarning( + Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, + V1SchemeSigner.MANIFEST_ENTRY_NAME, + jcaDigestAlgorithm, + mSignatureFileEntry.getName(), + Base64.getEncoder().encodeToString(actual), + Base64.getEncoder().encodeToString(expected)); + verified = false; + } + } + return verified; + } + + /** + * Verifies the digest of the manifest's main section against the main section of the .SF + * file. + */ + private void verifyManifestMainSectionDigest( + ManifestParser.Section sfMainSection, + ManifestParser.Section manifestMainSection, + byte[] manifestBytes, + int minSdkVersion, + int maxSdkVersion) throws NoSuchAlgorithmException { + Collection expectedDigests = + getDigestsToVerify( + sfMainSection, + "-Digest-Manifest-Main-Attributes", + minSdkVersion, + maxSdkVersion); + if (expectedDigests.isEmpty()) { + return; + } + + for (NamedDigest expectedDigest : expectedDigests) { + String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; + byte[] actual = + digest( + jcaDigestAlgorithm, + manifestBytes, + manifestMainSection.getStartOffset(), + manifestMainSection.getSizeBytes()); + byte[] expected = expectedDigest.digest; + if (!Arrays.equals(expected, actual)) { + mResult.addError( + Issue.JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY, + jcaDigestAlgorithm, + mSignatureFileEntry.getName(), + Base64.getEncoder().encodeToString(actual), + Base64.getEncoder().encodeToString(expected)); + } + } + } + + /** + * Verifies the digest of the manifest's individual section against the corresponding + * individual section of the .SF file. + */ + private void verifyManifestIndividualSectionDigest( + ManifestParser.Section sfIndividualSection, + boolean createdBySigntool, + ManifestParser.Section manifestIndividualSection, + byte[] manifestBytes, + int minSdkVersion, + int maxSdkVersion) throws NoSuchAlgorithmException { + String entryName = sfIndividualSection.getName(); + Collection expectedDigests = + getDigestsToVerify( + sfIndividualSection, "-Digest", minSdkVersion, maxSdkVersion); + if (expectedDigests.isEmpty()) { + mResult.addError( + Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE, + entryName, + mSignatureFileEntry.getName()); + return; + } + + int sectionStartIndex = manifestIndividualSection.getStartOffset(); + int sectionSizeBytes = manifestIndividualSection.getSizeBytes(); + if (createdBySigntool) { + int sectionEndIndex = sectionStartIndex + sectionSizeBytes; + if ((manifestBytes[sectionEndIndex - 1] == '\n') + && (manifestBytes[sectionEndIndex - 2] == '\n')) { + sectionSizeBytes--; + } + } + for (NamedDigest expectedDigest : expectedDigests) { + String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; + byte[] actual = + digest( + jcaDigestAlgorithm, + manifestBytes, + sectionStartIndex, + sectionSizeBytes); + byte[] expected = expectedDigest.digest; + if (!Arrays.equals(expected, actual)) { + mResult.addError( + Issue.JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY, + entryName, + jcaDigestAlgorithm, + mSignatureFileEntry.getName(), + Base64.getEncoder().encodeToString(actual), + Base64.getEncoder().encodeToString(expected)); + } + } + } + + private void checkForStrippedApkSignatures( + ManifestParser.Section sfMainSection, + Map supportedApkSigSchemeNames, + Set foundApkSigSchemeIds) { + String signedWithApkSchemes = + sfMainSection.getAttributeValue( + V1SchemeSigner.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); + // This field contains a comma-separated list of APK signature scheme IDs which were + // used to sign this APK. Android rejects APKs where an ID is known to the platform but + // the APK didn't verify using that scheme. + + if (signedWithApkSchemes == null) { + // APK signature (e.g., v2 scheme) stripping protections not enabled. + if (!foundApkSigSchemeIds.isEmpty()) { + // APK is signed with an APK signature scheme such as v2 scheme. + mResult.addWarning( + Issue.JAR_SIG_NO_APK_SIG_STRIP_PROTECTION, + mSignatureFileEntry.getName()); + } + return; + } + + if (supportedApkSigSchemeNames.isEmpty()) { + return; + } + + Set supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet(); + Set supportedExpectedApkSigSchemeIds = new HashSet<>(1); + StringTokenizer tokenizer = new StringTokenizer(signedWithApkSchemes, ","); + while (tokenizer.hasMoreTokens()) { + String idText = tokenizer.nextToken().trim(); + if (idText.isEmpty()) { + continue; + } + int id; + try { + id = Integer.parseInt(idText); + } catch (Exception ignored) { + continue; + } + // This APK was supposed to be signed with the APK signature scheme having + // this ID. + if (supportedApkSigSchemeIds.contains(id)) { + supportedExpectedApkSigSchemeIds.add(id); + } else { + mResult.addWarning( + Issue.JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID, + mSignatureFileEntry.getName(), + id); + } + } + + for (int id : supportedExpectedApkSigSchemeIds) { + if (!foundApkSigSchemeIds.contains(id)) { + String apkSigSchemeName = supportedApkSigSchemeNames.get(id); + mResult.addError( + Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED, + mSignatureFileEntry.getName(), + id, + apkSigSchemeName); + } + } + } + } + + public static Collection getDigestsToVerify( + ManifestParser.Section section, + String digestAttrSuffix, + int minSdkVersion, + int maxSdkVersion) { + Decoder base64Decoder = Base64.getDecoder(); + List result = new ArrayList<>(1); + if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) { + // Prior to JB MR2, Android platform's logic for picking a digest algorithm to verify is + // to rely on the ancient Digest-Algorithms attribute which contains + // whitespace-separated list of digest algorithms (defaulting to SHA-1) to try. The + // first digest attribute (with supported digest algorithm) found using the list is + // used. + String algs = section.getAttributeValue("Digest-Algorithms"); + if (algs == null) { + algs = "SHA SHA1"; + } + StringTokenizer tokens = new StringTokenizer(algs); + while (tokens.hasMoreTokens()) { + String alg = tokens.nextToken(); + String attrName = alg + digestAttrSuffix; + String digestBase64 = section.getAttributeValue(attrName); + if (digestBase64 == null) { + // Attribute not found + continue; + } + alg = getCanonicalJcaMessageDigestAlgorithm(alg); + if ((alg == null) + || (getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(alg) + > minSdkVersion)) { + // Unsupported digest algorithm + continue; + } + // Supported digest algorithm + result.add(new NamedDigest(alg, base64Decoder.decode(digestBase64))); + break; + } + // No supported digests found -- this will fail to verify on pre-JB MR2 Androids. + if (result.isEmpty()) { + return result; + } + } + + if (maxSdkVersion >= AndroidSdkVersion.JELLY_BEAN_MR2) { + // On JB MR2 and newer, Android platform picks the strongest algorithm out of: + // SHA-512, SHA-384, SHA-256, SHA-1. + for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) { + String attrName = getJarDigestAttributeName(alg, digestAttrSuffix); + String digestBase64 = section.getAttributeValue(attrName); + if (digestBase64 == null) { + // Attribute not found + continue; + } + byte[] digest = base64Decoder.decode(digestBase64); + byte[] digestInResult = getDigest(result, alg); + if ((digestInResult == null) || (!Arrays.equals(digestInResult, digest))) { + result.add(new NamedDigest(alg, digest)); + } + break; + } + } + + return result; + } + + private static final String[] JB_MR2_AND_NEWER_DIGEST_ALGS = { + "SHA-512", + "SHA-384", + "SHA-256", + "SHA-1", + }; + + private static String getCanonicalJcaMessageDigestAlgorithm(String algorithm) { + return UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.get(algorithm.toUpperCase(Locale.US)); + } + + public static int getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile( + String jcaAlgorithmName) { + Integer result = + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.get( + jcaAlgorithmName.toUpperCase(Locale.US)); + return (result != null) ? result : Integer.MAX_VALUE; + } + + private static String getJarDigestAttributeName( + String jcaDigestAlgorithm, String attrNameSuffix) { + if ("SHA-1".equalsIgnoreCase(jcaDigestAlgorithm)) { + return "SHA1" + attrNameSuffix; + } else { + return jcaDigestAlgorithm + attrNameSuffix; + } + } + + private static final Map UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL; + static { + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL = new HashMap<>(8); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("MD5", "MD5"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA", "SHA-1"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA1", "SHA-1"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-1", "SHA-1"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-256", "SHA-256"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-384", "SHA-384"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-512", "SHA-512"); + } + + private static final Map + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST; + static { + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST = new HashMap<>(5); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("MD5", 0); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-1", 0); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-256", 0); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( + "SHA-384", AndroidSdkVersion.GINGERBREAD); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( + "SHA-512", AndroidSdkVersion.GINGERBREAD); + } + + private static byte[] getDigest(Collection digests, String jcaDigestAlgorithm) { + for (NamedDigest digest : digests) { + if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) { + return digest.digest; + } + } + return null; + } + + public static List parseZipCentralDirectory( + DataSource apk, + ApkUtils.ZipSections apkSections) + throws IOException, ApkFormatException { + // Read the ZIP Central Directory + long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); + if (cdSizeBytes > Integer.MAX_VALUE) { + throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); + } + long cdOffset = apkSections.getZipCentralDirectoryOffset(); + ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); + cd.order(ByteOrder.LITTLE_ENDIAN); + + // Parse the ZIP Central Directory + int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); + List cdRecords = new ArrayList<>(expectedCdRecordCount); + for (int i = 0; i < expectedCdRecordCount; i++) { + CentralDirectoryRecord cdRecord; + int offsetInsideCd = cd.position(); + try { + cdRecord = CentralDirectoryRecord.getRecord(cd); + } catch (ZipFormatException e) { + throw new ApkFormatException( + "Malformed ZIP Central Directory record #" + (i + 1) + + " at file offset " + (cdOffset + offsetInsideCd), + e); + } + String entryName = cdRecord.getName(); + if (entryName.endsWith("/")) { + // Ignore directory entries + continue; + } + cdRecords.add(cdRecord); + } + // There may be more data in Central Directory, but we don't warn or throw because Android + // ignores unused CD data. + + return cdRecords; + } + + /** + * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's + * manifest for the APK to verify on Android. + */ + private static boolean isJarEntryDigestNeededInManifest(String entryName) { + // NOTE: This logic is different from what's required by the JAR signing scheme. This is + // because Android's APK verification logic differs from that spec. In particular, JAR + // signing spec includes into JAR manifest all files in subdirectories of META-INF and + // any files inside META-INF not related to signatures. + if (entryName.startsWith("META-INF/")) { + return false; + } + return !entryName.endsWith("/"); + } + + private static Set verifyJarEntriesAgainstManifestAndSigners( + DataSource apk, + long cdOffsetInApk, + Collection cdRecords, + Map entryNameToManifestSection, + List signers, + int minSdkVersion, + int maxSdkVersion, + Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException { + // Iterate over APK contents as sequentially as possible to improve performance. + List cdRecordsSortedByLocalFileHeaderOffset = + new ArrayList<>(cdRecords); + Collections.sort( + cdRecordsSortedByLocalFileHeaderOffset, + CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); + Set manifestEntryNamesMissingFromApk = + new HashSet<>(entryNameToManifestSection.keySet()); + List firstSignedEntrySigners = null; + String firstSignedEntryName = null; + for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) { + String entryName = cdRecord.getName(); + manifestEntryNamesMissingFromApk.remove(entryName); + if (!isJarEntryDigestNeededInManifest(entryName)) { + continue; + } + + ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName); + if (manifestSection == null) { + result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); + continue; + } + + List entrySigners = new ArrayList<>(signers.size()); + for (Signer signer : signers) { + if (signer.getSigFileEntryNames().contains(entryName)) { + entrySigners.add(signer); + } + } + if (entrySigners.isEmpty()) { + result.addError(Issue.JAR_SIG_ZIP_ENTRY_NOT_SIGNED, entryName); + continue; + } + if (firstSignedEntrySigners == null) { + firstSignedEntrySigners = entrySigners; + firstSignedEntryName = entryName; + } else if (!entrySigners.equals(firstSignedEntrySigners)) { + result.addError( + Issue.JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH, + firstSignedEntryName, + getSignerNames(firstSignedEntrySigners), + entryName, + getSignerNames(entrySigners)); + continue; + } + + List expectedDigests = + new ArrayList<>( + getDigestsToVerify( + manifestSection, "-Digest", minSdkVersion, maxSdkVersion)); + if (expectedDigests.isEmpty()) { + result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); + continue; + } + + MessageDigest[] mds = new MessageDigest[expectedDigests.size()]; + for (int i = 0; i < expectedDigests.size(); i++) { + mds[i] = getMessageDigest(expectedDigests.get(i).jcaDigestAlgorithm); + } + + try { + LocalFileRecord.outputUncompressedData( + apk, + cdRecord, + cdOffsetInApk, + DataSinks.asDataSink(mds)); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed ZIP entry: " + entryName, e); + } catch (IOException e) { + throw new IOException("Failed to read entry: " + entryName, e); + } + + for (int i = 0; i < expectedDigests.size(); i++) { + NamedDigest expectedDigest = expectedDigests.get(i); + byte[] actualDigest = mds[i].digest(); + if (!Arrays.equals(expectedDigest.digest, actualDigest)) { + result.addError( + Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, + entryName, + expectedDigest.jcaDigestAlgorithm, + V1SchemeSigner.MANIFEST_ENTRY_NAME, + Base64.getEncoder().encodeToString(actualDigest), + Base64.getEncoder().encodeToString(expectedDigest.digest)); + } + } + } + + if (firstSignedEntrySigners == null) { + result.addError(Issue.JAR_SIG_NO_SIGNED_ZIP_ENTRIES); + return Collections.emptySet(); + } else { + return new HashSet<>(firstSignedEntrySigners); + } + } + + private static List getSignerNames(List signers) { + if (signers.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(signers.size()); + for (Signer signer : signers) { + result.add(signer.getName()); + } + return result; + } + + private static MessageDigest getMessageDigest(String algorithm) + throws NoSuchAlgorithmException { + return MessageDigest.getInstance(algorithm); + } + + private static byte[] digest(String algorithm, byte[] data, int offset, int length) + throws NoSuchAlgorithmException { + MessageDigest md = getMessageDigest(algorithm); + md.update(data, offset, length); + return md.digest(); + } + + private static byte[] digest(String algorithm, byte[] data) throws NoSuchAlgorithmException { + return getMessageDigest(algorithm).digest(data); + } + + public static class NamedDigest { + public final String jcaDigestAlgorithm; + public final byte[] digest; + + private NamedDigest(String jcaDigestAlgorithm, byte[] digest) { + this.jcaDigestAlgorithm = jcaDigestAlgorithm; + this.digest = digest; + } + } + + public static class Result { + + /** Whether the APK's JAR signature verifies. */ + public boolean verified; + + /** List of APK's signers. These signers are used by Android. */ + public final List signers = new ArrayList<>(); + + /** + * Signers encountered in the APK but not included in the set of the APK's signers. These + * signers are ignored by Android. + */ + public final List ignoredSigners = new ArrayList<>(); + + private final List mWarnings = new ArrayList<>(); + private final List mErrors = new ArrayList<>(); + + private boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + for (SignerInfo signer : signers) { + if (signer.containsErrors()) { + return true; + } + } + return false; + } + + private void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + private void addWarning(Issue msg, Object... parameters) { + mWarnings.add(new IssueWithParams(msg, parameters)); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + + public static class SignerInfo { + public final String name; + public final String signatureFileName; + public final String signatureBlockFileName; + public final List certChain = new ArrayList<>(); + + private final List mWarnings = new ArrayList<>(); + private final List mErrors = new ArrayList<>(); + + private SignerInfo( + String name, String signatureBlockFileName, String signatureFileName) { + this.name = name; + this.signatureBlockFileName = signatureBlockFileName; + this.signatureFileName = signatureFileName; + } + + private boolean containsErrors() { + return !mErrors.isEmpty(); + } + + private void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + private void addWarning(Issue msg, Object... parameters) { + mWarnings.add(new IssueWithParams(msg, parameters)); + } + + public List getErrors() { + return mErrors; + } + + public List getWarnings() { + return mWarnings; + } + } + } + + private static class SignedAttributes { + private Map> mAttrs; + + public SignedAttributes(Collection attrs) throws Pkcs7DecodingException { + Map> result = new HashMap<>(attrs.size()); + for (Attribute attr : attrs) { + if (result.put(attr.attrType, attr.attrValues) != null) { + throw new Pkcs7DecodingException("Duplicate signed attribute: " + attr.attrType); + } + } + mAttrs = result; + } + + private Asn1OpaqueObject getSingleValue(String attrOid) throws Pkcs7DecodingException { + List values = mAttrs.get(attrOid); + if ((values == null) || (values.isEmpty())) { + return null; + } + if (values.size() > 1) { + throw new Pkcs7DecodingException("Attribute " + attrOid + " has multiple values"); + } + return values.get(0); + } + + public String getSingleObjectIdentifierValue(String attrOid) throws Pkcs7DecodingException { + Asn1OpaqueObject value = getSingleValue(attrOid); + if (value == null) { + return null; + } + try { + return Asn1BerParser.parse(value.getEncoded(), ObjectIdentifierChoice.class).value; + } catch (Asn1DecodingException e) { + throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e); + } + } + + public byte[] getSingleOctetStringValue(String attrOid) throws Pkcs7DecodingException { + Asn1OpaqueObject value = getSingleValue(attrOid); + if (value == null) { + return null; + } + try { + return Asn1BerParser.parse(value.getEncoded(), OctetStringChoice.class).value; + } catch (Asn1DecodingException e) { + throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e); + } + } + } + + @Asn1Class(type = Asn1Type.CHOICE) + public static class OctetStringChoice { + @Asn1Field(type = Asn1Type.OCTET_STRING) + public byte[] value; + } + + @Asn1Class(type = Asn1Type.CHOICE) + public static class ObjectIdentifierChoice { + @Asn1Field(type = Asn1Type.OBJECT_IDENTIFIER) + public String value; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java b/app/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java new file mode 100644 index 0000000..d8e4723 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java @@ -0,0 +1,294 @@ +/* + * 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. + * + *

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 APK Signature Scheme v2 + */ +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. + */ + + private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + + /** 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 getSuggestedSignatureAlgorithms( + PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported) + 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 algorithms = new ArrayList<>(); + algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256); + if (apkSigningBlockPaddingSupported) { + 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 algorithms = new ArrayList<>(); + algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256); + if (apkSigningBlockPaddingSupported) { + 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 algorithms = new ArrayList<>(); + algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256); + if (apkSigningBlockPaddingSupported) { + 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 Pair generateApkSignatureSchemeV2Block( + RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List signerConfigs, + boolean v3SigningEnabled) + throws IOException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + Pair, + Map> digestInfo = + ApkSigningBlockUtils.computeContentDigests( + executor, beforeCentralDir, centralDir, eocd, signerConfigs); + return generateApkSignatureSchemeV2Block( + digestInfo.getFirst(), digestInfo.getSecond(),v3SigningEnabled); + } + + private static Pair generateApkSignatureSchemeV2Block( + List signerConfigs, + Map contentDigests, + boolean v3SigningEnabled) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + // FORMAT: + // * length-prefixed sequence of length-prefixed signer blocks. + + List signerBlocks = new ArrayList<>(signerConfigs.size()); + 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), + }), APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + } + + private static byte[] generateSignerBlock( + SignerConfig signerConfig, + Map 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> 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, + }); + } + + // Attribute to check whether a newer APK Signature Scheme signature was stripped + protected static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d; + + 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(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> signatures; + public byte[] publicKey; + } + + private static final class SignedData { + public List> digests; + public List certificates; + public byte[] additionalAttributes; + } + } + +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/app/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java new file mode 100644 index 0000000..51c40bd --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java @@ -0,0 +1,462 @@ +/* + * 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. + * + *

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 APK Signature Scheme v2 + */ +public abstract class V2SchemeVerifier { + + private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + + /** 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()}. + * + *

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 supportedApkSigSchemeNames, + Set 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, + 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 supportedApkSigSchemeNames, + Set foundSigSchemeIds, + int minSdkVersion, + int maxSdkVersion, + ApkSigningBlockUtils.Result result) + throws IOException, NoSuchAlgorithmException { + Set 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}. + * + *

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}). + * + *

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 parseSigners( + ByteBuffer apkSignatureSchemeV2Block, + Set contentDigestsToVerify, + Map supportedApkSigSchemeNames, + Set 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}. + * + *

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. + * + *

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 contentDigestsToVerify, + Map supportedApkSigSchemeNames, + Set 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 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 signaturesToVerify = null; + try { + signaturesToVerify = + ApkSigningBlockUtils.getSignaturesToVerify( + supportedSignatures, minSdkVersion, maxSdkVersion); + } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) { + result.addError(Issue.V2_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.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 = 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 sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size()); + for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) { + sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId()); + } + List 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 supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet(); + Set 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 V2SchemeSigner.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); + } + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java b/app/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java new file mode 100644 index 0000000..722b304 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java @@ -0,0 +1,325 @@ +/* + * 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. + * + *

APK Signature Scheme v3 builds upon APK Signature Scheme v3, and maintains all of the APK + * Signature Scheme v2 goals. + * + * @see APK Signature Scheme v2 + * + *

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 = 0xf05368c0; + + /** 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 getSuggestedSignatureAlgorithms( + PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported) + 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 algorithms = new ArrayList<>(); + algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256); + if (apkSigningBlockPaddingSupported) { + 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 algorithms = new ArrayList<>(); + algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256); + if (apkSigningBlockPaddingSupported) { + 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 algorithms = new ArrayList<>(); + algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256); + if (apkSigningBlockPaddingSupported) { + 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 Pair generateApkSignatureSchemeV3Block( + RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List signerConfigs) + throws IOException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + Pair, + Map> digestInfo = + ApkSigningBlockUtils.computeContentDigests( + executor, beforeCentralDir, centralDir, eocd, signerConfigs); + return generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond()); + } + + private static Pair generateApkSignatureSchemeV3Block( + List signerConfigs, + Map contentDigests) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + // FORMAT: + // * length-prefixed sequence of length-prefixed signer blocks. + + List 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), + }), APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + } + + private static byte[] generateSignerBlock( + SignerConfig signerConfig, + Map 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> 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(); + } + + public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c; + + private static byte[] generateAdditionalAttributes(SignerConfig signerConfig) { + if (signerConfig.mSigningCertificateLineage == null) { + return new byte[0]; + } + return signerConfig.mSigningCertificateLineage.generateV3SignerAttribute(); + } + + private static final class V3SignatureSchemeBlock { + private static final class Signer { + public byte[] signedData; + public int minSdkVersion; + public int maxSdkVersion; + public List> signatures; + public byte[] publicKey; + } + + private static final class SignedData { + public List> digests; + public List certificates; + public int minSdkVersion; + public int maxSdkVersion; + public byte[] additionalAttributes; + } + } + +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/app/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java new file mode 100644 index 0000000..16a6408 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java @@ -0,0 +1,518 @@ +/* + * 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.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.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. + * + *

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 APK Signature Scheme v2 + */ +public abstract class V3SchemeVerifier { + + private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; + + /** 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()}. + * + *

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, + 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 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 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 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}. + * + *

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}). + * + *

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 parseSigners( + ByteBuffer apkSignatureSchemeV3Block, + Set 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}. + * + *

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. + * + *

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 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 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 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 = 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 sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size()); + for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) { + sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId()); + } + List 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 == V3SchemeSigner.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; + } + } + } + +} diff --git a/app/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java b/app/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java new file mode 100644 index 0000000..e1e01a9 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java @@ -0,0 +1,306 @@ +/* + * 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; + +/** + * APK Signer Lineage. + * + *

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. + * + *

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 Application Signing + */ +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 readSigningCertificateLineage(ByteBuffer inputBytes) + throws IOException { + List 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 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 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 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; + } + + /** + * 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; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/app/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java new file mode 100644 index 0000000..fdbd6eb --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java @@ -0,0 +1,674 @@ +/* + * 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. + * + *

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: + *

    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • The class must expose a public no-arg constructor.
  • + *
  • Member fields of the class which are populated with parsed input must be + * annotated with {@link Asn1Field} and be public and non-final.
  • + *
+ * + * @throws Asn1DecodingException if the input could not be decoded into the specified Java + * object + */ + public static T parse(ByteBuffer encoded, Class 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}. + * + *

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: + *

    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • The class must expose a public no-arg constructor.
  • + *
  • Member fields of the class which are populated with parsed input must be + * annotated with {@link Asn1Field} and be public and non-final.
  • + *
+ * + * @throws Asn1DecodingException if the input could not be decoded into the specified Java + * object + */ + public static List parseImplicitSetOf(ByteBuffer encoded, Class 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 parse(BerDataValue container, Class 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 parseChoice(BerDataValue dataValue, Class containerClass) + throws Asn1DecodingException { + List 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 parseSequence(BerDataValue container, Class containerClass) + throws Asn1DecodingException { + return parseSequence(container, containerClass, false); + } + + private static T parseSequence(BerDataValue container, Class containerClass, + boolean isUnencodedContainer) throws Asn1DecodingException { + List 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 List parseSetOf(BerDataValue container, Class elementClass) + throws Asn1DecodingException { + List 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); + try { + return value.intValue(); + } catch (ArithmeticException e) { + throw new Asn1DecodingException( + String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value), e); + } + } + + private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException { + BigInteger value = integerToBigInteger(encoded); + try { + return value.longValue(); + } catch (ArithmeticException e) { + throw new Asn1DecodingException( + String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value), + e); + } + } + + private static List getAnnotatedFields(Class containerClass) + throws Asn1DecodingException { + Field[] declaredFields = containerClass.getDeclaredFields(); + List 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 convert( + Asn1Type sourceType, + BerDataValue dataValue, + Class 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()); + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java b/app/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java new file mode 100644 index 0000000..4841296 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java @@ -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(); +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java b/app/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java new file mode 100644 index 0000000..0788642 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java @@ -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); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java b/app/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java new file mode 100644 index 0000000..22a432f --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java @@ -0,0 +1,593 @@ +/* + * 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. + * + *

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: + *

    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • Member fields of the class which are to be encoded must be annotated with + * {@link Asn1Field} and be public.
  • + *
+ * + * @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 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 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 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 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 { + 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 getAnnotatedFields(Object container) + throws Asn1EncodingException { + Class containerClass = container.getClass(); + Field[] declaredFields = containerClass.getDeclaredFields(); + List 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); + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java b/app/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java new file mode 100644 index 0000000..0002c25 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java @@ -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); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java b/app/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java new file mode 100644 index 0000000..d2d3ce0 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java @@ -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; +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java b/app/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java new file mode 100644 index 0000000..672d0e7 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java @@ -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(); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java b/app/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java new file mode 100644 index 0000000..6cdfcf0 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java @@ -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, +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java b/app/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java new file mode 100644 index 0000000..35fa374 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java @@ -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, +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java b/app/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java new file mode 100644 index 0000000..7300622 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java @@ -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 +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java b/app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java new file mode 100644 index 0000000..f5604ff --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java @@ -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; + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java b/app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java new file mode 100644 index 0000000..11ef6c3 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java @@ -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); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java b/app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java new file mode 100644 index 0000000..8da0a42 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java @@ -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. + * + *

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; +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java b/app/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java new file mode 100644 index 0000000..d32330c --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java @@ -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); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java b/app/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java new file mode 100644 index 0000000..3fd5291 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java @@ -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"); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java b/app/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java new file mode 100644 index 0000000..5fbca51 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java @@ -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; + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/jar/ManifestParser.java b/app/src/main/java/com/android/apksig/internal/jar/ManifestParser.java new file mode 100644 index 0000000..ab0a5da --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/jar/ManifestParser.java @@ -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. + * + *

These files consist of a main section followed by individual sections. Individual sections + * are named, their names referring to JAR entries. + * + * @see JAR Manifest format + */ +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

readAllSections() { + List
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 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 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 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 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; + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java b/app/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java new file mode 100644 index 0000000..fa01beb --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java @@ -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 JAR Manifest format + */ +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 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 getAttributesSortedByName(Attributes attributes) { + Set> attributesEntries = attributes.entrySet(); + SortedMap namedAttributes = new TreeMap(); + for (Map.Entry attribute : attributesEntries) { + String attrName = attribute.getKey().toString(); + String attrValue = attribute.getValue().toString(); + namedAttributes.put(attrName, attrValue); + } + return namedAttributes; + } + + static void writeAttributes( + OutputStream out, SortedMap attributesSortedByName) throws IOException { + for (Map.Entry attribute : attributesSortedByName.entrySet()) { + String attrName = attribute.getKey(); + String attrValue = attribute.getValue(); + writeAttribute(out, attrName, attrValue); + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java b/app/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java new file mode 100644 index 0000000..fd8cbff --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java @@ -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 JAR Manifest format + */ +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 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); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java b/app/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java new file mode 100644 index 0000000..39bce94 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java @@ -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.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.Asn1Type; + +/** + * 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; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java b/app/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java new file mode 100644 index 0000000..a6c91ef --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java @@ -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 attrValues; +} diff --git a/app/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java b/app/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java new file mode 100644 index 0000000..8ab722c --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java @@ -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; +} diff --git a/app/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java b/app/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java new file mode 100644 index 0000000..79f41af --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java @@ -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; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java b/app/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java new file mode 100644 index 0000000..284b117 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java @@ -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; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java b/app/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java new file mode 100644 index 0000000..1a115d5 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java @@ -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"; +} diff --git a/app/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java b/app/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java new file mode 100644 index 0000000..4004ee7 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java @@ -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); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java b/app/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java new file mode 100644 index 0000000..56b6e50 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java @@ -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 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 certificates; + + @Asn1Field( + index = 4, + type = Asn1Type.SET_OF, + tagging = Asn1Tagging.IMPLICIT, tagNumber = 1, + optional = true) + public List crls; + + @Asn1Field(index = 5, type = Asn1Type.SET_OF) + public List signerInfos; +} diff --git a/app/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java b/app/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java new file mode 100644 index 0000000..a3d70f1 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java @@ -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; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java b/app/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java new file mode 100644 index 0000000..b885eb8 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java @@ -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 unsignedAttrs; +} diff --git a/app/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java b/app/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java new file mode 100644 index 0000000..615d251 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java @@ -0,0 +1,50 @@ +/* + * 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 2.3. */ + public static final int GINGERBREAD = 9; + + /** 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; +} diff --git a/app/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java b/app/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java new file mode 100644 index 0000000..e5741a5 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java @@ -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 + + ")"); + } + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java b/app/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java new file mode 100644 index 0000000..656c20e --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java @@ -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 +")"); + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java b/app/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java new file mode 100644 index 0000000..d7cbe03 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java @@ -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); + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java b/app/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java new file mode 100644 index 0000000..a7b4b5c --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java @@ -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; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/ByteStreams.java b/app/src/main/java/com/android/apksig/internal/util/ByteStreams.java new file mode 100644 index 0000000..bca3b08 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/ByteStreams.java @@ -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(); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java b/app/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java new file mode 100644 index 0000000..a0baf1a --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java @@ -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 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 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 sources = new ArrayList<>(); + sources.add(beginSource.slice( + beginLocalOffset, beginSource.size() - beginLocalOffset)); + + Pair 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 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); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java b/app/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java new file mode 100644 index 0000000..8f9e1fd --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java @@ -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 getCriticalExtensionOIDs() { + return mDelegate.getCriticalExtensionOIDs(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return mDelegate.getExtensionValue(oid); + } + + @Override + public Set 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 getExtendedKeyUsage() throws CertificateParsingException { + return mDelegate.getExtendedKeyUsage(); + } + + @Override + public Collection> getSubjectAlternativeNames() throws CertificateParsingException { + return mDelegate.getSubjectAlternativeNames(); + } + + @Override + public Collection> getIssuerAlternativeNames() throws CertificateParsingException { + return mDelegate.getIssuerAlternativeNames(); + } + + @Override + public void verify(PublicKey key, Provider sigProvider) throws CertificateException, + NoSuchAlgorithmException, InvalidKeyException, SignatureException { + mDelegate.verify(key, sigProvider); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java b/app/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java new file mode 100644 index 0000000..958cd12 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +/** + * {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction + * time. + */ +public class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate { + private static final long serialVersionUID = 1L; + + private final byte[] mEncodedForm; + private int mHash = -1; + + public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) { + super(wrapped); + this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null; + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return (mEncodedForm != null) ? mEncodedForm.clone() : null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof X509Certificate)) return false; + + try { + byte[] a = this.getEncoded(); + byte[] b = ((X509Certificate) o).getEncoded(); + return Arrays.equals(a, b); + } catch (CertificateEncodingException e) { + return false; + } + } + + @Override + public int hashCode() { + if (mHash == -1) { + try { + mHash = Arrays.hashCode(this.getEncoded()); + } catch (CertificateEncodingException e) { + mHash = 0; + } + } + return mHash; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java b/app/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java new file mode 100644 index 0000000..d7866a9 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Inclusive interval of integers. + */ +public class InclusiveIntRange { + private final int min; + private final int max; + + private InclusiveIntRange(int min, int max) { + this.min = min; + this.max = max; + } + + public int getMin() { + return min; + } + + public int getMax() { + return max; + } + + public static InclusiveIntRange fromTo(int min, int max) { + return new InclusiveIntRange(min, max); + } + + public static InclusiveIntRange from(int min) { + return new InclusiveIntRange(min, Integer.MAX_VALUE); + } + + public List getValuesNotIn( + List sortedNonOverlappingRanges) { + if (sortedNonOverlappingRanges.isEmpty()) { + return Collections.singletonList(this); + } + + int testValue = min; + List result = null; + for (InclusiveIntRange range : sortedNonOverlappingRanges) { + int rangeMax = range.max; + if (testValue > rangeMax) { + continue; + } + int rangeMin = range.min; + if (testValue < range.min) { + if (result == null) { + result = new ArrayList<>(); + } + result.add(fromTo(testValue, rangeMin - 1)); + } + if (rangeMax >= max) { + return (result != null) ? result : Collections.emptyList(); + } + testValue = rangeMax + 1; + } + if (testValue <= max) { + if (result == null) { + result = new ArrayList<>(1); + } + result.add(fromTo(testValue, max)); + } + return (result != null) ? result : Collections.emptyList(); + } + + @Override + public String toString() { + return "[" + min + ", " + ((max < Integer.MAX_VALUE) ? (max + "]") : "\u221e)"); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java b/app/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java new file mode 100644 index 0000000..733dd56 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import java.nio.ByteBuffer; +import java.security.MessageDigest; + +/** + * Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each + * {@code MessageDigest} instance receives the same data. + */ +public class MessageDigestSink implements DataSink { + + private final MessageDigest[] mMessageDigests; + + public MessageDigestSink(MessageDigest[] digests) { + mMessageDigests = digests; + } + + @Override + public void consume(byte[] buf, int offset, int length) { + for (MessageDigest md : mMessageDigests) { + md.update(buf, offset, length); + } + } + + @Override + public void consume(ByteBuffer buf) { + int originalPosition = buf.position(); + for (MessageDigest md : mMessageDigests) { + // Reset the position back to the original because the previous iteration's + // MessageDigest.update set the buffer's position to the buffer's limit. + buf.position(originalPosition); + md.update(buf); + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java b/app/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java new file mode 100644 index 0000000..f1b5ac6 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * {@link DataSink} which outputs received data into the associated {@link OutputStream}. + */ +public class OutputStreamDataSink implements DataSink { + + private static final int MAX_READ_CHUNK_SIZE = 65536; + + private final OutputStream mOut; + + /** + * Constructs a new {@code OutputStreamDataSink} which outputs received data into the provided + * {@link OutputStream}. + */ + public OutputStreamDataSink(OutputStream out) { + if (out == null) { + throw new NullPointerException("out == null"); + } + mOut = out; + } + + /** + * Returns {@link OutputStream} into which this data sink outputs received data. + */ + public OutputStream getOutputStream() { + return mOut; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + mOut.write(buf, offset, length); + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + if (!buf.hasRemaining()) { + return; + } + + if (buf.hasArray()) { + mOut.write( + buf.array(), + buf.arrayOffset() + buf.position(), + buf.remaining()); + buf.position(buf.limit()); + } else { + byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)]; + while (buf.hasRemaining()) { + int chunkSize = Math.min(buf.remaining(), tmp.length); + buf.get(tmp, 0, chunkSize); + mOut.write(tmp, 0, chunkSize); + } + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/Pair.java b/app/src/main/java/com/android/apksig/internal/util/Pair.java new file mode 100644 index 0000000..7f9ee52 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/Pair.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +/** + * Pair of two elements. + */ +public final class Pair { + private final A mFirst; + private final B mSecond; + + private Pair(A first, B second) { + mFirst = first; + mSecond = second; + } + + public static Pair of(A first, B second) { + return new Pair(first, second); + } + + public A getFirst() { + return mFirst; + } + + public B getSecond() { + return mSecond; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); + result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + @SuppressWarnings("rawtypes") + Pair other = (Pair) obj; + if (mFirst == null) { + if (other.mFirst != null) { + return false; + } + } else if (!mFirst.equals(other.mFirst)) { + return false; + } + if (mSecond == null) { + if (other.mSecond != null) { + return false; + } + } else if (!mSecond.equals(other.mSecond)) { + return false; + } + return true; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java b/app/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java new file mode 100644 index 0000000..bbd2d14 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * {@link DataSink} which outputs received data into the associated file, sequentially. + */ +public class RandomAccessFileDataSink implements DataSink { + + private final RandomAccessFile mFile; + private final FileChannel mFileChannel; + private long mPosition; + + /** + * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the + * beginning of the provided file. + */ + public RandomAccessFileDataSink(RandomAccessFile file) { + this(file, 0); + } + + /** + * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the + * specified position of the provided file. + */ + public RandomAccessFileDataSink(RandomAccessFile file, long startPosition) { + if (file == null) { + throw new NullPointerException("file == null"); + } + if (startPosition < 0) { + throw new IllegalArgumentException("startPosition: " + startPosition); + } + mFile = file; + mFileChannel = file.getChannel(); + mPosition = startPosition; + } + + /** + * Returns the underlying {@link RandomAccessFile}. + */ + public RandomAccessFile getFile() { + return mFile; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + if (offset < 0) { + // Must perform this check here because RandomAccessFile.write doesn't throw when offset + // is negative but length is 0 + throw new IndexOutOfBoundsException("offset: " + offset); + } + if (offset > buf.length) { + // Must perform this check here because RandomAccessFile.write doesn't throw when offset + // is too large but length is 0 + throw new IndexOutOfBoundsException( + "offset: " + offset + ", buf.length: " + buf.length); + } + if (length == 0) { + return; + } + + synchronized (mFile) { + mFile.seek(mPosition); + mFile.write(buf, offset, length); + mPosition += length; + } + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + int length = buf.remaining(); + if (length == 0) { + return; + } + + synchronized (mFile) { + mFile.seek(mPosition); + while (buf.hasRemaining()) { + mFileChannel.write(buf); + } + mPosition += length; + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSource.java b/app/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSource.java new file mode 100644 index 0000000..9c75d26 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSource.java @@ -0,0 +1,195 @@ +/* + * 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 RandomAccessFileDataSource 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 RandomAccessFileDataSource} 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 RandomAccessFileDataSource(RandomAccessFile file) { + mChannel = file.getChannel(); + mOffset = 0; + mSize = -1; + } + + /** + * Constructs a new {@code RandomAccessFileDataSource} 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 RandomAccessFileDataSource(RandomAccessFile file, long offset, long size) { + this(file.getChannel(), offset, size); + } + + private RandomAccessFileDataSource(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 RandomAccessFileDataSource slice(long offset, long size) { + long sourceSize = size(); + checkChunkValid(offset, size, sourceSize); + if ((offset == 0) && (size == sourceSize)) { + return this; + } + + return new RandomAccessFileDataSource(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 +")"); + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/TeeDataSink.java b/app/src/main/java/com/android/apksig/internal/util/TeeDataSink.java new file mode 100644 index 0000000..2e46f18 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/TeeDataSink.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * {@link DataSink} which copies provided input into each of the sinks provided to it. + */ +public class TeeDataSink implements DataSink { + + private final DataSink[] mSinks; + + public TeeDataSink(DataSink[] sinks) { + mSinks = sinks; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + for (DataSink sink : mSinks) { + sink.consume(buf, offset, length); + } + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + int originalPosition = buf.position(); + for (int i = 0; i < mSinks.length; i++) { + if (i > 0) { + buf.position(originalPosition); + } + mSinks[i].consume(buf); + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/app/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java new file mode 100644 index 0000000..75497a7 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java @@ -0,0 +1,216 @@ +/* + * 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.internal.zip.ZipUtils; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * VerityTreeBuilder is used to generate the root hash of verity tree built from the input file. + * The root hash can be used on device for on-access verification. The tree itself is reproducible + * on device, and is not shipped with the APK. + */ +public class VerityTreeBuilder { + + /** Maximum size (in bytes) of each node of the tree. */ + private final static int CHUNK_SIZE = 4096; + + /** Digest algorithm (JCA Digest algorithm name) used in the tree. */ + private final static String JCA_ALGORITHM = "SHA-256"; + + /** Optional salt to apply before each digestion. */ + private final byte[] mSalt; + + private final MessageDigest mMd; + + public VerityTreeBuilder(byte[] salt) throws NoSuchAlgorithmException { + mSalt = salt; + mMd = MessageDigest.getInstance(JCA_ALGORITHM); + } + + /** + * Returns the root hash of the APK verity tree built from ZIP blocks. + * + * Specifically, APK verity tree is built from the APK, but as if the APK Signing Block (which + * must be page aligned) and the "Central Directory offset" field in End of Central Directory + * are skipped. + */ + public byte[] generateVerityTreeRootHash(DataSource beforeApkSigningBlock, + DataSource centralDir, DataSource eocd) throws IOException { + if (beforeApkSigningBlock.size() % CHUNK_SIZE != 0) { + throw new IllegalStateException("APK Signing Block size not a multiple of " + CHUNK_SIZE + + ": " + beforeApkSigningBlock.size()); + } + + // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory + // offset field is treated as pointing to the offset at which the APK Signing Block will + // start. + long centralDirOffsetForDigesting = beforeApkSigningBlock.size(); + ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size()); + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + eocd.copyTo(0, (int) eocd.size(), eocdBuf); + eocdBuf.flip(); + ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting); + + return generateVerityTreeRootHash(new ChainedDataSource(beforeApkSigningBlock, centralDir, + DataSources.asDataSource(eocdBuf))); + } + + /** + * Returns the root hash of the verity tree built from the data source. + * + * The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the + * input file. If the total size is larger than 4 KB, take this level as input and repeat the + * same procedure, until the level is within 4 KB. If salt is given, it will apply to each + * digestion before the actual data. + * + * The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt. + * + * The tree is currently stored only in memory and is never written out. Nevertheless, it is + * the actual verity tree format on disk, and is supposed to be re-generated on device. + * + * This is package-private for testing purpose. + */ + byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException { + int digestSize = mMd.getDigestLength(); + + // Calculate the summed area table of level size. In other word, this is the offset + // table of each level, plus the next non-existing level. + int[] levelOffset = calculateLevelOffset(fileSource.size(), digestSize); + + ByteBuffer verityBuffer = ByteBuffer.allocate(levelOffset[levelOffset.length - 1]); + + // Generate the hash tree bottom-up. + for (int i = levelOffset.length - 2; i >= 0; i--) { + DataSink middleBufferSink = new ByteBufferSink( + slice(verityBuffer, levelOffset[i], levelOffset[i + 1])); + DataSource src; + if (i == levelOffset.length - 2) { + src = fileSource; + digestDataByChunks(src, middleBufferSink); + } else { + src = DataSources.asDataSource(slice(verityBuffer.asReadOnlyBuffer(), + levelOffset[i + 1], levelOffset[i + 2])); + digestDataByChunks(src, middleBufferSink); + } + + // If the output is not full chunk, pad with 0s. + long totalOutput = divideRoundup(src.size(), CHUNK_SIZE) * digestSize; + int incomplete = (int) (totalOutput % CHUNK_SIZE); + if (incomplete > 0) { + byte[] padding = new byte[CHUNK_SIZE - incomplete]; + middleBufferSink.consume(padding, 0, padding.length); + } + } + + // Finally, calculate the root hash from the top level (only page). + ByteBuffer firstPage = slice(verityBuffer.asReadOnlyBuffer(), 0, CHUNK_SIZE); + return saltedDigest(firstPage); + } + + /** + * Returns an array of summed area table of level size in the verity tree. In other words, the + * returned array is offset of each level in the verity tree file format, plus an additional + * offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size + * is level + 1. + */ + private static int[] calculateLevelOffset(long dataSize, int digestSize) { + // Compute total size of each level, bottom to top. + ArrayList levelSize = new ArrayList<>(); + while (true) { + long chunkCount = divideRoundup(dataSize, CHUNK_SIZE); + long size = CHUNK_SIZE * divideRoundup(chunkCount * digestSize, CHUNK_SIZE); + levelSize.add(size); + if (chunkCount * digestSize <= CHUNK_SIZE) { + break; + } + dataSize = chunkCount * digestSize; + } + + // Reverse and convert to summed area table. + int[] levelOffset = new int[levelSize.size() + 1]; + levelOffset[0] = 0; + for (int i = 0; i < levelSize.size(); i++) { + // We don't support verity tree if it is larger then Integer.MAX_VALUE. + levelOffset[i + 1] = levelOffset[i] + Math.toIntExact( + levelSize.get(levelSize.size() - i - 1)); + } + return levelOffset; + } + + /** + * Digest data source by chunks then feeds them to the sink one by one. If the last unit is + * less than the chunk size and padding is desired, feed with extra padding 0 to fill up the + * chunk before digesting. + */ + private void digestDataByChunks(DataSource dataSource, DataSink dataSink) throws IOException { + long size = dataSource.size(); + long offset = 0; + for (; offset + CHUNK_SIZE <= size; offset += CHUNK_SIZE) { + ByteBuffer buffer = ByteBuffer.allocate(CHUNK_SIZE); + dataSource.copyTo(offset, CHUNK_SIZE, buffer); + buffer.rewind(); + byte[] hash = saltedDigest(buffer); + dataSink.consume(hash, 0, hash.length); + } + + // Send the last incomplete chunk with 0 padding to the sink at once. + int remaining = (int) (size % CHUNK_SIZE); + if (remaining > 0) { + ByteBuffer buffer; + buffer = ByteBuffer.allocate(CHUNK_SIZE); // initialized to 0. + dataSource.copyTo(offset, remaining, buffer); + buffer.rewind(); + + byte[] hash = saltedDigest(buffer); + dataSink.consume(hash, 0, hash.length); + } + } + + /** Returns the digest of data with salt prepanded. */ + private byte[] saltedDigest(ByteBuffer data) { + mMd.reset(); + if (mSalt != null) { + mMd.update(mSalt); + } + mMd.update(data); + return mMd.digest(); + } + + /** Divides a number and round up to the closest integer. */ + private static long divideRoundup(long dividend, long divisor) { + return (dividend + divisor - 1) / divisor; + } + + /** Returns a slice of the buffer with shared the content. */ + private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) { + ByteBuffer b = buffer.duplicate(); + b.position(0); // to ensure position <= limit invariant. + b.limit(end); + b.position(begin); + return b.slice(); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java b/app/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java new file mode 100644 index 0000000..9a266f2 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java @@ -0,0 +1,278 @@ +/* + * 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.util; + +import com.android.apksig.internal.asn1.Asn1BerParser; +import com.android.apksig.internal.asn1.Asn1DecodingException; +import com.android.apksig.internal.asn1.Asn1DerEncoder; +import com.android.apksig.internal.asn1.Asn1EncodingException; +import com.android.apksig.internal.x509.Certificate; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; + +/** + * Provides methods to generate {@code X509Certificate}s from their encoded form. These methods + * can be used to generate certificates that would be rejected by the Java {@code + * CertificateFactory}. + */ +public class X509CertificateUtils { + + private static CertificateFactory sCertFactory = null; + + // The PEM certificate header and footer as specified in RFC 7468: + // There is exactly one space character (SP) separating the "BEGIN" or + // "END" from the label. There are exactly five hyphen-minus (also + // known as dash) characters ("-") on both ends of the encapsulation + // boundaries, no more, no less. + public static final byte[] BEGIN_CERT_HEADER = "-----BEGIN CERTIFICATE-----".getBytes(); + public static final byte[] END_CERT_FOOTER = "-----END CERTIFICATE-----".getBytes(); + + private static void buildCertFactory() { + if (sCertFactory != null) { + return; + } + try { + sCertFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to create X.509 CertificateFactory", e); + } + } + + /** + * Generates an {@code X509Certificate} from the {@code InputStream}. + * + * @throws CertificateException if the {@code InputStream} cannot be decoded to a valid + * certificate. + */ + public static X509Certificate generateCertificate(InputStream in) throws CertificateException { + byte[] encodedForm; + try { + encodedForm = ByteStreams.toByteArray(in); + } catch (IOException e) { + throw new CertificateException("Failed to parse certificate", e); + } + return generateCertificate(encodedForm); + } + + /** + * Generates an {@code X509Certificate} from the encoded form. + * + * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate. + */ + public static X509Certificate generateCertificate(byte[] encodedForm) + throws CertificateException { + if (sCertFactory == null) { + buildCertFactory(); + } + return generateCertificate(encodedForm, sCertFactory); + } + + /** + * Generates an {@code X509Certificate} from the encoded form using the provided + * {@code CertificateFactory}. + * + * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate. + */ + public static X509Certificate generateCertificate(byte[] encodedForm, + CertificateFactory certFactory) throws CertificateException { + X509Certificate certificate; + try { + certificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(encodedForm)); + return certificate; + } catch (CertificateException e) { + // This could be expected if the certificate is encoded using a BER encoding that does + // not use the minimum number of bytes to represent the length of the contents; attempt + // to decode the certificate using the BER parser and re-encode using the DER encoder + // below. + } + try { + // Some apps were previously signed with a BER encoded certificate that now results + // in exceptions from the CertificateFactory generateCertificate(s) methods. Since + // the original BER encoding of the certificate is used as the signature for these + // apps that original encoding must be maintained when signing updated versions of + // these apps and any new apps that may require capabilities guarded by the + // signature. To maintain the same signature the BER parser can be used to parse + // the certificate, then it can be re-encoded to its DER equivalent which is + // accepted by the generateCertificate method. The positions in the ByteBuffer can + // then be used with the GuaranteedEncodedFormX509Certificate object to ensure the + // getEncoded method returns the original signature of the app. + ByteBuffer encodedCertBuffer = getNextDEREncodedCertificateBlock( + ByteBuffer.wrap(encodedForm)); + int startingPos = encodedCertBuffer.position(); + Certificate reencodedCert = Asn1BerParser.parse(encodedCertBuffer, Certificate.class); + byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert); + certificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(reencodedForm)); + // If the reencodedForm is successfully accepted by the CertificateFactory then copy the + // original encoding from the ByteBuffer and use that encoding in the Guaranteed object. + byte[] originalEncoding = new byte[encodedCertBuffer.position() - startingPos]; + encodedCertBuffer.position(startingPos); + encodedCertBuffer.get(originalEncoding); + GuaranteedEncodedFormX509Certificate guaranteedEncodedCert = + new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding); + return guaranteedEncodedCert; + } catch (Asn1DecodingException | Asn1EncodingException | CertificateException e) { + throw new CertificateException("Failed to parse certificate", e); + } + } + + /** + * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code + * InputStream}. + * + * @throws CertificateException if the InputStream cannot be decoded to zero or more valid + * {@code Certificate} objects. + */ + public static Collection generateCertificates( + InputStream in) throws CertificateException { + if (sCertFactory == null) { + buildCertFactory(); + } + return generateCertificates(in, sCertFactory); + } + + /** + * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code + * InputStream} using the provided {@code CertificateFactory}. + * + * @throws CertificateException if the InputStream cannot be decoded to zero or more valid + * {@code Certificates} objects. + */ + public static Collection generateCertificates( + InputStream in, CertificateFactory certFactory) throws CertificateException { + // Since the InputStream is not guaranteed to support mark / reset operations first read it + // into a byte array to allow using the BER parser / DER encoder if it cannot be read by + // the CertificateFactory. + byte[] encodedCerts; + try { + encodedCerts = ByteStreams.toByteArray(in); + } catch (IOException e) { + throw new CertificateException("Failed to read the input stream", e); + } + try { + return certFactory.generateCertificates(new ByteArrayInputStream(encodedCerts)); + } catch (CertificateException e) { + // This could be expected if the certificates are encoded using a BER encoding that does + // not use the minimum number of bytes to represent the length of the contents; attempt + // to decode the certificates using the BER parser and re-encode using the DER encoder + // below. + } + try { + Collection certificates = new ArrayList<>(1); + ByteBuffer encodedCertsBuffer = ByteBuffer.wrap(encodedCerts); + while (encodedCertsBuffer.hasRemaining()) { + ByteBuffer certBuffer = getNextDEREncodedCertificateBlock(encodedCertsBuffer); + int startingPos = certBuffer.position(); + Certificate reencodedCert = Asn1BerParser.parse(certBuffer, Certificate.class); + byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert); + X509Certificate certificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(reencodedForm)); + byte[] originalEncoding = new byte[certBuffer.position() - startingPos]; + certBuffer.position(startingPos); + certBuffer.get(originalEncoding); + GuaranteedEncodedFormX509Certificate guaranteedEncodedCert = + new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding); + certificates.add(guaranteedEncodedCert); + } + return certificates; + } catch (Asn1DecodingException | Asn1EncodingException e) { + throw new CertificateException("Failed to parse certificates", e); + } + } + + /** + * Parses the provided ByteBuffer to obtain the next certificate in DER encoding. If the buffer + * does not begin with the PEM certificate header then it is returned with the assumption that + * it is already DER encoded. If the buffer does begin with the PEM certificate header then the + * certificate data is read from the buffer until the PEM certificate footer is reached; this + * data is then base64 decoded and returned in a new ByteBuffer. + * + * If the buffer is in PEM format then the position of the buffer is moved to the end of the + * current certificate; if the buffer is already DER encoded then the position of the buffer is + * not modified. + * + * @throws CertificateException if the buffer contains the PEM certificate header but does not + * contain the expected footer. + */ + private static ByteBuffer getNextDEREncodedCertificateBlock(ByteBuffer certificateBuffer) + throws CertificateException { + if (certificateBuffer == null) { + throw new NullPointerException("The certificateBuffer cannot be null"); + } + // if the buffer does not contain enough data for the PEM cert header then just return the + // provided buffer. + if (certificateBuffer.remaining() < BEGIN_CERT_HEADER.length) { + return certificateBuffer; + } + certificateBuffer.mark(); + for (int i = 0; i < BEGIN_CERT_HEADER.length; i++) { + if (certificateBuffer.get() != BEGIN_CERT_HEADER[i]) { + certificateBuffer.reset(); + return certificateBuffer; + } + } + StringBuilder pemEncoding = new StringBuilder(); + while (certificateBuffer.hasRemaining()) { + char encodedChar = (char) certificateBuffer.get(); + // if the current character is a '-' then the beginning of the footer has been reached + if (encodedChar == '-') { + break; + } else if (Character.isWhitespace(encodedChar)) { + continue; + } else { + pemEncoding.append(encodedChar); + } + } + // start from the second index in the certificate footer since the first '-' should have + // been consumed above. + for (int i = 1; i < END_CERT_FOOTER.length; i++) { + if (!certificateBuffer.hasRemaining()) { + throw new CertificateException( + "The provided input contains the PEM certificate header but does not " + + "contain sufficient data for the footer"); + } + if (certificateBuffer.get() != END_CERT_FOOTER[i]) { + throw new CertificateException( + "The provided input contains the PEM certificate header without a " + + "valid certificate footer"); + } + } + byte[] derEncoding = Base64.getDecoder().decode(pemEncoding.toString()); + // consume any trailing whitespace in the byte buffer + int nextEncodedChar = certificateBuffer.position(); + while (certificateBuffer.hasRemaining()) { + char trailingChar = (char) certificateBuffer.get(); + if (Character.isWhitespace(trailingChar)) { + nextEncodedChar++; + } else { + break; + } + } + certificateBuffer.position(nextEncodedChar); + return ByteBuffer.wrap(derEncoding); + } +} diff --git a/app/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java b/app/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java new file mode 100644 index 0000000..077db23 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java @@ -0,0 +1,35 @@ +/* + * 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.x509; + +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; + +/** + * {@code AttributeTypeAndValue} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class AttributeTypeAndValue { + + @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER) + public String attrType; + + @Asn1Field(index = 1, type = Asn1Type.ANY) + public Asn1OpaqueObject attrValue; +} \ No newline at end of file diff --git a/app/src/main/java/com/android/apksig/internal/x509/Certificate.java b/app/src/main/java/com/android/apksig/internal/x509/Certificate.java new file mode 100644 index 0000000..abb3c15 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/x509/Certificate.java @@ -0,0 +1,39 @@ +/* + * 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.x509; + +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.pkcs7.AlgorithmIdentifier; + +import java.nio.ByteBuffer; + +/** + * X509 {@code Certificate} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class Certificate { + @Asn1Field(index = 0, type = Asn1Type.SEQUENCE) + public TBSCertificate certificate; + + @Asn1Field(index = 1, type = Asn1Type.SEQUENCE) + public AlgorithmIdentifier signatureAlgorithm; + + @Asn1Field(index = 2, type = Asn1Type.BIT_STRING) + public ByteBuffer signature; +} diff --git a/app/src/main/java/com/android/apksig/internal/x509/Extension.java b/app/src/main/java/com/android/apksig/internal/x509/Extension.java new file mode 100644 index 0000000..bf37c1e --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/x509/Extension.java @@ -0,0 +1,38 @@ +/* + * 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.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +import java.nio.ByteBuffer; + +/** + * X509 {@code Extension} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class Extension { + @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER) + public String extensionID; + + @Asn1Field(index = 1, type = Asn1Type.BOOLEAN, optional = true) + public boolean isCritial = false; + + @Asn1Field(index = 2, type = Asn1Type.OCTET_STRING) + public ByteBuffer extensionValue; +} diff --git a/app/src/main/java/com/android/apksig/internal/x509/Name.java b/app/src/main/java/com/android/apksig/internal/x509/Name.java new file mode 100644 index 0000000..08400d6 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/x509/Name.java @@ -0,0 +1,34 @@ +/* + * 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.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +import java.util.List; + +/** + * X501 {@code Name} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.CHOICE) +public class Name { + + // This field is the RDNSequence specified in RFC 5280. + @Asn1Field(index = 0, type = Asn1Type.SEQUENCE_OF) + public List relativeDistinguishedNames; +} diff --git a/app/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java b/app/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java new file mode 100644 index 0000000..bb89e8d --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java @@ -0,0 +1,33 @@ +/* + * 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.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +import java.util.List; + +/** + * {@code RelativeDistinguishedName} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.UNENCODED_CONTAINER) +public class RelativeDistinguishedName { + + @Asn1Field(index = 0, type = Asn1Type.SET_OF) + public List attributes; +} diff --git a/app/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java b/app/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java new file mode 100644 index 0000000..8215237 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java @@ -0,0 +1,36 @@ +/* + * 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.x509; + +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.pkcs7.AlgorithmIdentifier; + +import java.nio.ByteBuffer; + +/** + * {@code SubjectPublicKeyInfo} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class SubjectPublicKeyInfo { + @Asn1Field(index = 0, type = Asn1Type.SEQUENCE) + public AlgorithmIdentifier algorithmIdentifier; + + @Asn1Field(index = 1, type = Asn1Type.BIT_STRING) + public ByteBuffer subjectPublicKey; +} diff --git a/app/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java b/app/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java new file mode 100644 index 0000000..922f52c --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java @@ -0,0 +1,79 @@ +/* + * 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.x509; + +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 com.android.apksig.internal.pkcs7.AlgorithmIdentifier; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * To Be Signed Certificate as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class TBSCertificate { + + @Asn1Field( + index = 0, + type = Asn1Type.INTEGER, + tagging = Asn1Tagging.EXPLICIT, tagNumber = 0) + public int version; + + @Asn1Field(index = 1, type = Asn1Type.INTEGER) + public BigInteger serialNumber; + + @Asn1Field(index = 2, type = Asn1Type.SEQUENCE) + public AlgorithmIdentifier signatureAlgorithm; + + @Asn1Field(index = 3, type = Asn1Type.CHOICE) + public Name issuer; + + @Asn1Field(index = 4, type = Asn1Type.SEQUENCE) + public Validity validity; + + @Asn1Field(index = 5, type = Asn1Type.CHOICE) + public Name subject; + + @Asn1Field(index = 6, type = Asn1Type.SEQUENCE) + public SubjectPublicKeyInfo subjectPublicKeyInfo; + + @Asn1Field(index = 7, + type = Asn1Type.BIT_STRING, + tagging = Asn1Tagging.IMPLICIT, + optional = true, + tagNumber = 1) + public ByteBuffer issuerUniqueID; + + @Asn1Field(index = 8, + type = Asn1Type.BIT_STRING, + tagging = Asn1Tagging.IMPLICIT, + optional = true, + tagNumber = 2) + public ByteBuffer subjectUniqueID; + + @Asn1Field(index = 9, + type = Asn1Type.SEQUENCE_OF, + tagging = Asn1Tagging.EXPLICIT, + optional = true, + tagNumber = 3) + public List extensions; +} diff --git a/app/src/main/java/com/android/apksig/internal/x509/Time.java b/app/src/main/java/com/android/apksig/internal/x509/Time.java new file mode 100644 index 0000000..def2ee8 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/x509/Time.java @@ -0,0 +1,34 @@ +/* + * 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.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +/** + * {@code Time} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.CHOICE) +public class Time { + + @Asn1Field(type = Asn1Type.UTC_TIME) + public String utcTime; + + @Asn1Field(type = Asn1Type.GENERALIZED_TIME) + public String generalizedTime; +} diff --git a/app/src/main/java/com/android/apksig/internal/x509/Validity.java b/app/src/main/java/com/android/apksig/internal/x509/Validity.java new file mode 100644 index 0000000..df9acb3 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/x509/Validity.java @@ -0,0 +1,34 @@ +/* + * 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.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +/** + * {@code Validity} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class Validity { + + @Asn1Field(index = 0, type = Asn1Type.CHOICE) + public Time notBefore; + + @Asn1Field(index = 1, type = Asn1Type.CHOICE) + public Time notAfter; +} diff --git a/app/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java b/app/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java new file mode 100644 index 0000000..d2f444d --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java @@ -0,0 +1,304 @@ +/* + * 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.zip; + +import com.android.apksig.zip.ZipFormatException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; + +/** + * ZIP Central Directory (CD) Record. + */ +public class CentralDirectoryRecord { + + /** + * Comparator which compares records by the offset of the corresponding Local File Header in the + * archive. + */ + public static final Comparator BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR = + new ByLocalFileHeaderOffsetComparator(); + + private static final int RECORD_SIGNATURE = 0x02014b50; + private static final int HEADER_SIZE_BYTES = 46; + + private static final int GP_FLAGS_OFFSET = 8; + private static final int LOCAL_FILE_HEADER_OFFSET_OFFSET = 42; + private static final int NAME_OFFSET = HEADER_SIZE_BYTES; + + private final ByteBuffer mData; + private final short mGpFlags; + private final short mCompressionMethod; + private final int mLastModificationTime; + private final int mLastModificationDate; + private final long mCrc32; + private final long mCompressedSize; + private final long mUncompressedSize; + private final long mLocalFileHeaderOffset; + private final String mName; + private final int mNameSizeBytes; + + private CentralDirectoryRecord( + ByteBuffer data, + short gpFlags, + short compressionMethod, + int lastModificationTime, + int lastModificationDate, + long crc32, + long compressedSize, + long uncompressedSize, + long localFileHeaderOffset, + String name, + int nameSizeBytes) { + mData = data; + mGpFlags = gpFlags; + mCompressionMethod = compressionMethod; + mLastModificationDate = lastModificationDate; + mLastModificationTime = lastModificationTime; + mCrc32 = crc32; + mCompressedSize = compressedSize; + mUncompressedSize = uncompressedSize; + mLocalFileHeaderOffset = localFileHeaderOffset; + mName = name; + mNameSizeBytes = nameSizeBytes; + } + + public int getSize() { + return mData.remaining(); + } + + public String getName() { + return mName; + } + + public int getNameSizeBytes() { + return mNameSizeBytes; + } + + public short getGpFlags() { + return mGpFlags; + } + + public short getCompressionMethod() { + return mCompressionMethod; + } + + public int getLastModificationTime() { + return mLastModificationTime; + } + + public int getLastModificationDate() { + return mLastModificationDate; + } + + public long getCrc32() { + return mCrc32; + } + + public long getCompressedSize() { + return mCompressedSize; + } + + public long getUncompressedSize() { + return mUncompressedSize; + } + + public long getLocalFileHeaderOffset() { + return mLocalFileHeaderOffset; + } + + /** + * Returns the Central Directory Record starting at the current position of the provided buffer + * and advances the buffer's position immediately past the end of the record. + */ + public static CentralDirectoryRecord getRecord(ByteBuffer buf) throws ZipFormatException { + ZipUtils.assertByteOrderLittleEndian(buf); + if (buf.remaining() < HEADER_SIZE_BYTES) { + throw new ZipFormatException( + "Input too short. Need at least: " + HEADER_SIZE_BYTES + + " bytes, available: " + buf.remaining() + " bytes", + new BufferUnderflowException()); + } + int originalPosition = buf.position(); + int recordSignature = buf.getInt(); + if (recordSignature != RECORD_SIGNATURE) { + throw new ZipFormatException( + "Not a Central Directory record. Signature: 0x" + + Long.toHexString(recordSignature & 0xffffffffL)); + } + buf.position(originalPosition + GP_FLAGS_OFFSET); + short gpFlags = buf.getShort(); + short compressionMethod = buf.getShort(); + int lastModificationTime = ZipUtils.getUnsignedInt16(buf); + int lastModificationDate = ZipUtils.getUnsignedInt16(buf); + long crc32 = ZipUtils.getUnsignedInt32(buf); + long compressedSize = ZipUtils.getUnsignedInt32(buf); + long uncompressedSize = ZipUtils.getUnsignedInt32(buf); + int nameSize = ZipUtils.getUnsignedInt16(buf); + int extraSize = ZipUtils.getUnsignedInt16(buf); + int commentSize = ZipUtils.getUnsignedInt16(buf); + buf.position(originalPosition + LOCAL_FILE_HEADER_OFFSET_OFFSET); + long localFileHeaderOffset = ZipUtils.getUnsignedInt32(buf); + buf.position(originalPosition); + int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize; + if (recordSize > buf.remaining()) { + throw new ZipFormatException( + "Input too short. Need: " + recordSize + " bytes, available: " + + buf.remaining() + " bytes", + new BufferUnderflowException()); + } + String name = getName(buf, originalPosition + NAME_OFFSET, nameSize); + buf.position(originalPosition); + int originalLimit = buf.limit(); + int recordEndInBuf = originalPosition + recordSize; + ByteBuffer recordBuf; + try { + buf.limit(recordEndInBuf); + recordBuf = buf.slice(); + } finally { + buf.limit(originalLimit); + } + // Consume this record + buf.position(recordEndInBuf); + return new CentralDirectoryRecord( + recordBuf, + gpFlags, + compressionMethod, + lastModificationTime, + lastModificationDate, + crc32, + compressedSize, + uncompressedSize, + localFileHeaderOffset, + name, + nameSize); + } + + public void copyTo(ByteBuffer output) { + output.put(mData.slice()); + } + + public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset( + long localFileHeaderOffset) { + ByteBuffer result = ByteBuffer.allocate(mData.remaining()); + result.put(mData.slice()); + result.flip(); + result.order(ByteOrder.LITTLE_ENDIAN); + ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset); + return new CentralDirectoryRecord( + result, + mGpFlags, + mCompressionMethod, + mLastModificationTime, + mLastModificationDate, + mCrc32, + mCompressedSize, + mUncompressedSize, + localFileHeaderOffset, + mName, + mNameSizeBytes); + } + + public static CentralDirectoryRecord createWithDeflateCompressedData( + String name, + int lastModifiedTime, + int lastModifiedDate, + long crc32, + long compressedSize, + long uncompressedSize, + long localFileHeaderOffset) { + byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); + short gpFlags = ZipUtils.GP_FLAG_EFS; // UTF-8 character encoding used for entry name + short compressionMethod = ZipUtils.COMPRESSION_METHOD_DEFLATED; + int recordSize = HEADER_SIZE_BYTES + nameBytes.length; + ByteBuffer result = ByteBuffer.allocate(recordSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(RECORD_SIGNATURE); + ZipUtils.putUnsignedInt16(result, 0x14); // Version made by + ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract + result.putShort(gpFlags); + result.putShort(compressionMethod); + ZipUtils.putUnsignedInt16(result, lastModifiedTime); + ZipUtils.putUnsignedInt16(result, lastModifiedDate); + ZipUtils.putUnsignedInt32(result, crc32); + ZipUtils.putUnsignedInt32(result, compressedSize); + ZipUtils.putUnsignedInt32(result, uncompressedSize); + ZipUtils.putUnsignedInt16(result, nameBytes.length); + ZipUtils.putUnsignedInt16(result, 0); // Extra field length + ZipUtils.putUnsignedInt16(result, 0); // File comment length + ZipUtils.putUnsignedInt16(result, 0); // Disk number + ZipUtils.putUnsignedInt16(result, 0); // Internal file attributes + ZipUtils.putUnsignedInt32(result, 0); // External file attributes + ZipUtils.putUnsignedInt32(result, localFileHeaderOffset); + result.put(nameBytes); + + if (result.hasRemaining()) { + throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit()); + } + result.flip(); + return new CentralDirectoryRecord( + result, + gpFlags, + compressionMethod, + lastModifiedTime, + lastModifiedDate, + crc32, + compressedSize, + uncompressedSize, + localFileHeaderOffset, + name, + nameBytes.length); + } + + static String getName(ByteBuffer record, int position, int nameLengthBytes) { + byte[] nameBytes; + int nameBytesOffset; + if (record.hasArray()) { + nameBytes = record.array(); + nameBytesOffset = record.arrayOffset() + position; + } else { + nameBytes = new byte[nameLengthBytes]; + nameBytesOffset = 0; + int originalPosition = record.position(); + try { + record.position(position); + record.get(nameBytes); + } finally { + record.position(originalPosition); + } + } + return new String(nameBytes, nameBytesOffset, nameLengthBytes, StandardCharsets.UTF_8); + } + + private static class ByLocalFileHeaderOffsetComparator + implements Comparator { + @Override + public int compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2) { + long offset1 = r1.getLocalFileHeaderOffset(); + long offset2 = r2.getLocalFileHeaderOffset(); + if (offset1 > offset2) { + return 1; + } else if (offset1 < offset2) { + return -1; + } else { + return 0; + } + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/zip/EocdRecord.java b/app/src/main/java/com/android/apksig/internal/zip/EocdRecord.java new file mode 100644 index 0000000..9c531f4 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/zip/EocdRecord.java @@ -0,0 +1,48 @@ +/* + * 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.zip; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * ZIP End of Central Directory record. + */ +public class EocdRecord { + private static final int CD_RECORD_COUNT_ON_DISK_OFFSET = 8; + private static final int CD_RECORD_COUNT_TOTAL_OFFSET = 10; + private static final int CD_SIZE_OFFSET = 12; + private static final int CD_OFFSET_OFFSET = 16; + + public static ByteBuffer createWithModifiedCentralDirectoryInfo( + ByteBuffer original, + int centralDirectoryRecordCount, + long centralDirectorySizeBytes, + long centralDirectoryOffset) { + ByteBuffer result = ByteBuffer.allocate(original.remaining()); + result.order(ByteOrder.LITTLE_ENDIAN); + result.put(original.slice()); + result.flip(); + ZipUtils.setUnsignedInt16( + result, CD_RECORD_COUNT_ON_DISK_OFFSET, centralDirectoryRecordCount); + ZipUtils.setUnsignedInt16( + result, CD_RECORD_COUNT_TOTAL_OFFSET, centralDirectoryRecordCount); + ZipUtils.setUnsignedInt32(result, CD_SIZE_OFFSET, centralDirectorySizeBytes); + ZipUtils.setUnsignedInt32(result, CD_OFFSET_OFFSET, centralDirectoryOffset); + return result; + } +} diff --git a/app/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java b/app/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java new file mode 100644 index 0000000..0a55b1a --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java @@ -0,0 +1,537 @@ +/* + * 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.zip; + +import com.android.apksig.internal.util.ByteBufferSink; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * ZIP Local File record. + * + *

The record consists of the Local File Header, file data, and (if present) Data Descriptor. + */ +public class LocalFileRecord { + private static final int RECORD_SIGNATURE = 0x04034b50; + private static final int HEADER_SIZE_BYTES = 30; + + private static final int GP_FLAGS_OFFSET = 6; + private static final int CRC32_OFFSET = 14; + private static final int COMPRESSED_SIZE_OFFSET = 18; + private static final int UNCOMPRESSED_SIZE_OFFSET = 22; + private static final int NAME_LENGTH_OFFSET = 26; + private static final int EXTRA_LENGTH_OFFSET = 28; + private static final int NAME_OFFSET = HEADER_SIZE_BYTES; + + private static final int DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE = 12; + private static final int DATA_DESCRIPTOR_SIGNATURE = 0x08074b50; + + private final String mName; + private final int mNameSizeBytes; + private final ByteBuffer mExtra; + + private final long mStartOffsetInArchive; + private final long mSize; + + private final int mDataStartOffset; + private final long mDataSize; + private final boolean mDataCompressed; + private final long mUncompressedDataSize; + + private LocalFileRecord( + String name, + int nameSizeBytes, + ByteBuffer extra, + long startOffsetInArchive, + long size, + int dataStartOffset, + long dataSize, + boolean dataCompressed, + long uncompressedDataSize) { + mName = name; + mNameSizeBytes = nameSizeBytes; + mExtra = extra; + mStartOffsetInArchive = startOffsetInArchive; + mSize = size; + mDataStartOffset = dataStartOffset; + mDataSize = dataSize; + mDataCompressed = dataCompressed; + mUncompressedDataSize = uncompressedDataSize; + } + + public String getName() { + return mName; + } + + public ByteBuffer getExtra() { + return (mExtra.capacity() > 0) ? mExtra.slice() : mExtra; + } + + public int getExtraFieldStartOffsetInsideRecord() { + return HEADER_SIZE_BYTES + mNameSizeBytes; + } + + public long getStartOffsetInArchive() { + return mStartOffsetInArchive; + } + + public int getDataStartOffsetInRecord() { + return mDataStartOffset; + } + + /** + * Returns the size (in bytes) of this record. + */ + public long getSize() { + return mSize; + } + + /** + * Returns {@code true} if this record's file data is stored in compressed form. + */ + public boolean isDataCompressed() { + return mDataCompressed; + } + + /** + * Returns the Local File record starting at the current position of the provided buffer + * and advances the buffer's position immediately past the end of the record. The record + * consists of the Local File Header, data, and (if present) Data Descriptor. + */ + public static LocalFileRecord getRecord( + DataSource apk, + CentralDirectoryRecord cdRecord, + long cdStartOffset) throws ZipFormatException, IOException { + return getRecord( + apk, + cdRecord, + cdStartOffset, + true, // obtain extra field contents + true // include Data Descriptor (if present) + ); + } + + /** + * Returns the Local File record starting at the current position of the provided buffer + * and advances the buffer's position immediately past the end of the record. The record + * consists of the Local File Header, data, and (if present) Data Descriptor. + */ + private static LocalFileRecord getRecord( + DataSource apk, + CentralDirectoryRecord cdRecord, + long cdStartOffset, + boolean extraFieldContentsNeeded, + boolean dataDescriptorIncluded) throws ZipFormatException, IOException { + // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform + // exhibited when reading an APK for the purposes of verifying its signatures. + + String entryName = cdRecord.getName(); + int cdRecordEntryNameSizeBytes = cdRecord.getNameSizeBytes(); + int headerSizeWithName = HEADER_SIZE_BYTES + cdRecordEntryNameSizeBytes; + long headerStartOffset = cdRecord.getLocalFileHeaderOffset(); + long headerEndOffset = headerStartOffset + headerSizeWithName; + if (headerEndOffset > cdStartOffset) { + throw new ZipFormatException( + "Local File Header of " + entryName + " extends beyond start of Central" + + " Directory. LFH end: " + headerEndOffset + + ", CD start: " + cdStartOffset); + } + ByteBuffer header; + try { + header = apk.getByteBuffer(headerStartOffset, headerSizeWithName); + } catch (IOException e) { + throw new IOException("Failed to read Local File Header of " + entryName, e); + } + header.order(ByteOrder.LITTLE_ENDIAN); + + int recordSignature = header.getInt(); + if (recordSignature != RECORD_SIGNATURE) { + throw new ZipFormatException( + "Not a Local File Header record for entry " + entryName + ". Signature: 0x" + + Long.toHexString(recordSignature & 0xffffffffL)); + } + short gpFlags = header.getShort(GP_FLAGS_OFFSET); + boolean dataDescriptorUsed = (gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0; + boolean cdDataDescriptorUsed = + (cdRecord.getGpFlags() & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0; + if (dataDescriptorUsed != cdDataDescriptorUsed) { + throw new ZipFormatException( + "Data Descriptor presence mismatch between Local File Header and Central" + + " Directory for entry " + entryName + + ". LFH: " + dataDescriptorUsed + ", CD: " + cdDataDescriptorUsed); + } + long uncompressedDataCrc32FromCdRecord = cdRecord.getCrc32(); + long compressedDataSizeFromCdRecord = cdRecord.getCompressedSize(); + long uncompressedDataSizeFromCdRecord = cdRecord.getUncompressedSize(); + if (!dataDescriptorUsed) { + long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET); + if (crc32 != uncompressedDataCrc32FromCdRecord) { + throw new ZipFormatException( + "CRC-32 mismatch between Local File Header and Central Directory for entry " + + entryName + ". LFH: " + crc32 + + ", CD: " + uncompressedDataCrc32FromCdRecord); + } + long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET); + if (compressedSize != compressedDataSizeFromCdRecord) { + throw new ZipFormatException( + "Compressed size mismatch between Local File Header and Central Directory" + + " for entry " + entryName + ". LFH: " + compressedSize + + ", CD: " + compressedDataSizeFromCdRecord); + } + long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET); + if (uncompressedSize != uncompressedDataSizeFromCdRecord) { + throw new ZipFormatException( + "Uncompressed size mismatch between Local File Header and Central Directory" + + " for entry " + entryName + ". LFH: " + uncompressedSize + + ", CD: " + uncompressedDataSizeFromCdRecord); + } + } + int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET); + if (nameLength > cdRecordEntryNameSizeBytes) { + throw new ZipFormatException( + "Name mismatch between Local File Header and Central Directory for entry" + + entryName + ". LFH: " + nameLength + + " bytes, CD: " + cdRecordEntryNameSizeBytes + " bytes"); + } + String name = CentralDirectoryRecord.getName(header, NAME_OFFSET, nameLength); + if (!entryName.equals(name)) { + throw new ZipFormatException( + "Name mismatch between Local File Header and Central Directory. LFH: \"" + + name + "\", CD: \"" + entryName + "\""); + } + int extraLength = ZipUtils.getUnsignedInt16(header, EXTRA_LENGTH_OFFSET); + long dataStartOffset = headerStartOffset + HEADER_SIZE_BYTES + nameLength + extraLength; + long dataSize; + boolean compressed = + (cdRecord.getCompressionMethod() != ZipUtils.COMPRESSION_METHOD_STORED); + if (compressed) { + dataSize = compressedDataSizeFromCdRecord; + } else { + dataSize = uncompressedDataSizeFromCdRecord; + } + long dataEndOffset = dataStartOffset + dataSize; + if (dataEndOffset > cdStartOffset) { + throw new ZipFormatException( + "Local File Header data of " + entryName + " overlaps with Central Directory" + + ". LFH data start: " + dataStartOffset + + ", LFH data end: " + dataEndOffset + ", CD start: " + cdStartOffset); + } + + ByteBuffer extra = EMPTY_BYTE_BUFFER; + if ((extraFieldContentsNeeded) && (extraLength > 0)) { + extra = apk.getByteBuffer( + headerStartOffset + HEADER_SIZE_BYTES + nameLength, extraLength); + } + + long recordEndOffset = dataEndOffset; + // Include the Data Descriptor (if requested and present) into the record. + if ((dataDescriptorIncluded) && ((gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0)) { + // The record's data is supposed to be followed by the Data Descriptor. Unfortunately, + // the descriptor's size is not known in advance because the spec lets the signature + // field (the first four bytes) be omitted. Thus, there's no 100% reliable way to tell + // how long the Data Descriptor record is. Most parsers (including Android) check + // whether the first four bytes look like Data Descriptor record signature and, if so, + // assume that it is indeed the record's signature. However, this is the wrong + // conclusion if the record's CRC-32 (next field after the signature) has the same value + // as the signature. In any case, we're doing what Android is doing. + long dataDescriptorEndOffset = + dataEndOffset + DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE; + if (dataDescriptorEndOffset > cdStartOffset) { + throw new ZipFormatException( + "Data Descriptor of " + entryName + " overlaps with Central Directory" + + ". Data Descriptor end: " + dataEndOffset + + ", CD start: " + cdStartOffset); + } + ByteBuffer dataDescriptorPotentialSig = apk.getByteBuffer(dataEndOffset, 4); + dataDescriptorPotentialSig.order(ByteOrder.LITTLE_ENDIAN); + if (dataDescriptorPotentialSig.getInt() == DATA_DESCRIPTOR_SIGNATURE) { + dataDescriptorEndOffset += 4; + if (dataDescriptorEndOffset > cdStartOffset) { + throw new ZipFormatException( + "Data Descriptor of " + entryName + " overlaps with Central Directory" + + ". Data Descriptor end: " + dataEndOffset + + ", CD start: " + cdStartOffset); + } + } + recordEndOffset = dataDescriptorEndOffset; + } + + long recordSize = recordEndOffset - headerStartOffset; + int dataStartOffsetInRecord = HEADER_SIZE_BYTES + nameLength + extraLength; + + return new LocalFileRecord( + entryName, + cdRecordEntryNameSizeBytes, + extra, + headerStartOffset, + recordSize, + dataStartOffsetInRecord, + dataSize, + compressed, + uncompressedDataSizeFromCdRecord); + } + + /** + * Outputs this record and returns returns the number of bytes output. + */ + public long outputRecord(DataSource sourceApk, DataSink output) throws IOException { + long size = getSize(); + sourceApk.feed(getStartOffsetInArchive(), size, output); + return size; + } + + /** + * Outputs this record, replacing its extra field with the provided one, and returns returns the + * number of bytes output. + */ + public long outputRecordWithModifiedExtra( + DataSource sourceApk, + ByteBuffer extra, + DataSink output) throws IOException { + long recordStartOffsetInSource = getStartOffsetInArchive(); + int extraStartOffsetInRecord = getExtraFieldStartOffsetInsideRecord(); + int extraSizeBytes = extra.remaining(); + int headerSize = extraStartOffsetInRecord + extraSizeBytes; + ByteBuffer header = ByteBuffer.allocate(headerSize); + header.order(ByteOrder.LITTLE_ENDIAN); + sourceApk.copyTo(recordStartOffsetInSource, extraStartOffsetInRecord, header); + header.put(extra.slice()); + header.flip(); + ZipUtils.setUnsignedInt16(header, EXTRA_LENGTH_OFFSET, extraSizeBytes); + + long outputByteCount = header.remaining(); + output.consume(header); + long remainingRecordSize = getSize() - mDataStartOffset; + sourceApk.feed(recordStartOffsetInSource + mDataStartOffset, remainingRecordSize, output); + outputByteCount += remainingRecordSize; + return outputByteCount; + } + + /** + * Outputs the specified Local File Header record with its data and returns the number of bytes + * output. + */ + public static long outputRecordWithDeflateCompressedData( + String name, + int lastModifiedTime, + int lastModifiedDate, + byte[] compressedData, + long crc32, + long uncompressedSize, + DataSink output) throws IOException { + byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); + int recordSize = HEADER_SIZE_BYTES + nameBytes.length; + ByteBuffer result = ByteBuffer.allocate(recordSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(RECORD_SIGNATURE); + ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract + result.putShort(ZipUtils.GP_FLAG_EFS); // General purpose flag: UTF-8 encoded name + result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED); + ZipUtils.putUnsignedInt16(result, lastModifiedTime); + ZipUtils.putUnsignedInt16(result, lastModifiedDate); + ZipUtils.putUnsignedInt32(result, crc32); + ZipUtils.putUnsignedInt32(result, compressedData.length); + ZipUtils.putUnsignedInt32(result, uncompressedSize); + ZipUtils.putUnsignedInt16(result, nameBytes.length); + ZipUtils.putUnsignedInt16(result, 0); // Extra field length + result.put(nameBytes); + if (result.hasRemaining()) { + throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit()); + } + result.flip(); + + long outputByteCount = result.remaining(); + output.consume(result); + outputByteCount += compressedData.length; + output.consume(compressedData, 0, compressedData.length); + return outputByteCount; + } + + private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0); + + /** + * Sends uncompressed data of this record into the the provided data sink. + */ + public void outputUncompressedData( + DataSource lfhSection, + DataSink sink) throws IOException, ZipFormatException { + long dataStartOffsetInArchive = mStartOffsetInArchive + mDataStartOffset; + try { + if (mDataCompressed) { + try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) { + lfhSection.feed(dataStartOffsetInArchive, mDataSize, inflateAdapter); + long actualUncompressedSize = inflateAdapter.getOutputByteCount(); + if (actualUncompressedSize != mUncompressedDataSize) { + throw new ZipFormatException( + "Unexpected size of uncompressed data of " + mName + + ". Expected: " + mUncompressedDataSize + " bytes" + + ", actual: " + actualUncompressedSize + " bytes"); + } + } catch (IOException e) { + if (e.getCause() instanceof DataFormatException) { + throw new ZipFormatException("Data of entry " + mName + " malformed", e); + } + throw e; + } + } else { + lfhSection.feed(dataStartOffsetInArchive, mDataSize, sink); + // No need to check whether output size is as expected because DataSource.feed is + // guaranteed to output exactly the number of bytes requested. + } + } catch (IOException e) { + throw new IOException( + "Failed to read data of " + ((mDataCompressed) ? "compressed" : "uncompressed") + + " entry " + mName, + e); + } + // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We + // thus don't check either. + } + + /** + * Sends uncompressed data pointed to by the provided ZIP Central Directory (CD) record into the + * provided data sink. + */ + public static void outputUncompressedData( + DataSource source, + CentralDirectoryRecord cdRecord, + long cdStartOffsetInArchive, + DataSink sink) throws ZipFormatException, IOException { + // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform + // exhibited when reading an APK for the purposes of verifying its signatures. + // When verifying an APK, Android doesn't care reading the extra field or the Data + // Descriptor. + LocalFileRecord lfhRecord = + getRecord( + source, + cdRecord, + cdStartOffsetInArchive, + false, // don't care about the extra field + false // don't read the Data Descriptor + ); + lfhRecord.outputUncompressedData(source, sink); + } + + /** + * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record. + */ + public static byte[] getUncompressedData( + DataSource source, + CentralDirectoryRecord cdRecord, + long cdStartOffsetInArchive) throws ZipFormatException, IOException { + if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) { + throw new IOException( + cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize()); + } + byte[] result = new byte[(int) cdRecord.getUncompressedSize()]; + ByteBuffer resultBuf = ByteBuffer.wrap(result); + ByteBufferSink resultSink = new ByteBufferSink(resultBuf); + outputUncompressedData( + source, + cdRecord, + cdStartOffsetInArchive, + resultSink); + return result; + } + + /** + * {@link DataSink} which inflates received data and outputs the deflated data into the provided + * delegate sink. + */ + private static class InflateSinkAdapter implements DataSink, Closeable { + private final DataSink mDelegate; + + private Inflater mInflater = new Inflater(true); + private byte[] mOutputBuffer; + private byte[] mInputBuffer; + private long mOutputByteCount; + private boolean mClosed; + + private InflateSinkAdapter(DataSink delegate) { + mDelegate = delegate; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + checkNotClosed(); + mInflater.setInput(buf, offset, length); + if (mOutputBuffer == null) { + mOutputBuffer = new byte[65536]; + } + while (!mInflater.finished()) { + int outputChunkSize; + try { + outputChunkSize = mInflater.inflate(mOutputBuffer); + } catch (DataFormatException e) { + throw new IOException("Failed to inflate data", e); + } + if (outputChunkSize == 0) { + return; + } + mDelegate.consume(mOutputBuffer, 0, outputChunkSize); + mOutputByteCount += outputChunkSize; + } + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + checkNotClosed(); + if (buf.hasArray()) { + consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); + buf.position(buf.limit()); + } else { + if (mInputBuffer == null) { + mInputBuffer = new byte[65536]; + } + while (buf.hasRemaining()) { + int chunkSize = Math.min(buf.remaining(), mInputBuffer.length); + buf.get(mInputBuffer, 0, chunkSize); + consume(mInputBuffer, 0, chunkSize); + } + } + } + + public long getOutputByteCount() { + return mOutputByteCount; + } + + @Override + public void close() throws IOException { + mClosed = true; + mInputBuffer = null; + mOutputBuffer = null; + if (mInflater != null) { + mInflater.end(); + mInflater = null; + } + } + + private void checkNotClosed() { + if (mClosed) { + throw new IllegalStateException("Closed"); + } + } + } +} diff --git a/app/src/main/java/com/android/apksig/internal/zip/ZipUtils.java b/app/src/main/java/com/android/apksig/internal/zip/ZipUtils.java new file mode 100644 index 0000000..272015a --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/zip/ZipUtils.java @@ -0,0 +1,325 @@ +/* + * 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.zip; + +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.zip.CRC32; +import java.util.zip.Deflater; + +/** + * Assorted ZIP format helpers. + * + *

NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte + * order of these buffers is little-endian. + */ +public abstract class ZipUtils { + private ZipUtils() {} + + public static final short COMPRESSION_METHOD_STORED = 0; + public static final short COMPRESSION_METHOD_DEFLATED = 8; + + public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08; + public static final short GP_FLAG_EFS = 0x0800; + + private static final int ZIP_EOCD_REC_MIN_SIZE = 22; + private static final int ZIP_EOCD_REC_SIG = 0x06054b50; + private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; + private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; + private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; + private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; + + private static final int UINT16_MAX_VALUE = 0xffff; + + /** + * Sets the offset of the start of the ZIP Central Directory in the archive. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static void setZipEocdCentralDirectoryOffset( + ByteBuffer zipEndOfCentralDirectory, long offset) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + setUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, + offset); + } + + /** + * Returns the offset of the start of the ZIP Central Directory in the archive. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); + } + + /** + * Returns the size (in bytes) of the ZIP Central Directory. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); + } + + /** + * Returns the total number of records in ZIP Central Directory. + * + *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static int getZipEocdCentralDirectoryTotalRecordCount( + ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt16( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET); + } + + /** + * Returns the ZIP End of Central Directory record of the provided ZIP file. + * + * @return contents of the ZIP End of Central Directory record and the record's offset in the + * file or {@code null} if the file does not contain the record. + * + * @throws IOException if an I/O error occurs while reading the file. + */ + public static Pair findZipEndOfCentralDirectoryRecord(DataSource zip) + throws IOException { + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + long fileSize = zip.size(); + if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { + return null; + } + + // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus + // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily + // reading more data. + Pair result = findZipEndOfCentralDirectoryRecord(zip, 0); + if (result != null) { + return result; + } + + // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment + // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because + // the comment length field is an unsigned 16-bit number. + return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); + } + + /** + * Returns the ZIP End of Central Directory record of the provided ZIP file. + * + * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted + * value is from 0 to 65535 inclusive. The smaller the value, the faster this method + * locates the record, provided its comment field is no longer than this value. + * + * @return contents of the ZIP End of Central Directory record and the record's offset in the + * file or {@code null} if the file does not contain the record. + * + * @throws IOException if an I/O error occurs while reading the file. + */ + private static Pair findZipEndOfCentralDirectoryRecord( + DataSource zip, int maxCommentSize) throws IOException { + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) { + throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize); + } + + long fileSize = zip.size(); + if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { + // No space for EoCD record in the file. + return null; + } + // Lower maxCommentSize if the file is too small. + maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); + + int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize; + long bufOffsetInFile = fileSize - maxEocdSize; + ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize); + buf.order(ByteOrder.LITTLE_ENDIAN); + int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); + if (eocdOffsetInBuf == -1) { + // No EoCD record found in the buffer + return null; + } + // EoCD found + buf.position(eocdOffsetInBuf); + ByteBuffer eocd = buf.slice(); + eocd.order(ByteOrder.LITTLE_ENDIAN); + return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf); + } + + /** + * Returns the position at which ZIP End of Central Directory record starts in the provided + * buffer or {@code -1} if the record is not present. + * + *

NOTE: Byte order of {@code zipContents} must be little-endian. + */ + private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { + assertByteOrderLittleEndian(zipContents); + + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + int archiveSize = zipContents.capacity(); + if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { + return -1; + } + int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); + int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; + for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; + expectedCommentLength++) { + int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; + if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { + int actualCommentLength = + getUnsignedInt16( + zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); + if (actualCommentLength == expectedCommentLength) { + return eocdStartPos; + } + } + } + + return -1; + } + + static void assertByteOrderLittleEndian(ByteBuffer buffer) { + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + } + + public static int getUnsignedInt16(ByteBuffer buffer, int offset) { + return buffer.getShort(offset) & 0xffff; + } + + public static int getUnsignedInt16(ByteBuffer buffer) { + return buffer.getShort() & 0xffff; + } + + static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) { + if ((value < 0) || (value > 0xffff)) { + throw new IllegalArgumentException("uint16 value of out range: " + value); + } + buffer.putShort(offset, (short) value); + } + + static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { + if ((value < 0) || (value > 0xffffffffL)) { + throw new IllegalArgumentException("uint32 value of out range: " + value); + } + buffer.putInt(offset, (int) value); + } + + public static void putUnsignedInt16(ByteBuffer buffer, int value) { + if ((value < 0) || (value > 0xffff)) { + throw new IllegalArgumentException("uint16 value of out range: " + value); + } + buffer.putShort((short) value); + } + + static long getUnsignedInt32(ByteBuffer buffer, int offset) { + return buffer.getInt(offset) & 0xffffffffL; + } + + static long getUnsignedInt32(ByteBuffer buffer) { + return buffer.getInt() & 0xffffffffL; + } + + static void putUnsignedInt32(ByteBuffer buffer, long value) { + if ((value < 0) || (value > 0xffffffffL)) { + throw new IllegalArgumentException("uint32 value of out range: " + value); + } + buffer.putInt((int) value); + } + + public static DeflateResult deflate(ByteBuffer input) { + byte[] inputBuf; + int inputOffset; + int inputLength = input.remaining(); + if (input.hasArray()) { + inputBuf = input.array(); + inputOffset = input.arrayOffset() + input.position(); + input.position(input.limit()); + } else { + inputBuf = new byte[inputLength]; + inputOffset = 0; + input.get(inputBuf); + } + CRC32 crc32 = new CRC32(); + crc32.update(inputBuf, inputOffset, inputLength); + long crc32Value = crc32.getValue(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Deflater deflater = new Deflater(9, true); + deflater.setInput(inputBuf, inputOffset, inputLength); + deflater.finish(); + byte[] buf = new byte[65536]; + while (!deflater.finished()) { + int chunkSize = deflater.deflate(buf); + out.write(buf, 0, chunkSize); + } + return new DeflateResult(inputLength, crc32Value, out.toByteArray()); + } + + public static class DeflateResult { + public final int inputSizeBytes; + public final long inputCrc32; + public final byte[] output; + + public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) { + this.inputSizeBytes = inputSizeBytes; + this.inputCrc32 = inputCrc32; + this.output = output; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/apksig/util/DataSink.java b/app/src/main/java/com/android/apksig/util/DataSink.java new file mode 100644 index 0000000..5042933 --- /dev/null +++ b/app/src/main/java/com/android/apksig/util/DataSink.java @@ -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.util; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Consumer of input data which may be provided in one go or in chunks. + */ +public interface DataSink { + + /** + * Consumes the provided chunk of data. + * + *

This data sink guarantees to not hold references to the provided buffer after this method + * terminates. + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} are negative, or if + * {@code offset + length} is greater than {@code buf.length}. + */ + void consume(byte[] buf, int offset, int length) throws IOException; + + /** + * Consumes all remaining data in the provided buffer and advances the buffer's position + * to the buffer's limit. + * + *

This data sink guarantees to not hold references to the provided buffer after this method + * terminates. + */ + void consume(ByteBuffer buf) throws IOException; +} diff --git a/app/src/main/java/com/android/apksig/util/DataSinks.java b/app/src/main/java/com/android/apksig/util/DataSinks.java new file mode 100644 index 0000000..d9562d8 --- /dev/null +++ b/app/src/main/java/com/android/apksig/util/DataSinks.java @@ -0,0 +1,75 @@ +/* + * 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.util; + +import com.android.apksig.internal.util.ByteArrayDataSink; +import com.android.apksig.internal.util.MessageDigestSink; +import com.android.apksig.internal.util.OutputStreamDataSink; +import com.android.apksig.internal.util.RandomAccessFileDataSink; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.security.MessageDigest; + +/** + * Utility methods for working with {@link DataSink} abstraction. + */ +public abstract class DataSinks { + private DataSinks() {} + + /** + * Returns a {@link DataSink} which outputs received data into the provided + * {@link OutputStream}. + */ + public static DataSink asDataSink(OutputStream out) { + return new OutputStreamDataSink(out); + } + + /** + * Returns a {@link DataSink} which outputs received data into the provided file, sequentially, + * starting at the beginning of the file. + */ + public static DataSink asDataSink(RandomAccessFile file) { + return new RandomAccessFileDataSink(file); + } + + /** + * Returns a {@link DataSink} which forwards data into the provided {@link MessageDigest} + * instances via their {@code update} method. Each {@code MessageDigest} instance receives the + * same data. + */ + public static DataSink asDataSink(MessageDigest... digests) { + return new MessageDigestSink(digests); + } + + /** + * Returns a new in-memory {@link DataSink} which exposes all data consumed so far via the + * {@link DataSource} interface. + */ + public static ReadableDataSink newInMemoryDataSink() { + return new ByteArrayDataSink(); + } + + /** + * Returns a new in-memory {@link DataSink} which exposes all data consumed so far via the + * {@link DataSource} interface. + * + * @param initialCapacity initial capacity in bytes + */ + public static ReadableDataSink newInMemoryDataSink(int initialCapacity) { + return new ByteArrayDataSink(initialCapacity); + } +} diff --git a/app/src/main/java/com/android/apksig/util/DataSource.java b/app/src/main/java/com/android/apksig/util/DataSource.java new file mode 100644 index 0000000..a89a87c --- /dev/null +++ b/app/src/main/java/com/android/apksig/util/DataSource.java @@ -0,0 +1,110 @@ +/* + * 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.util; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Abstract representation of a source of data. + * + *

This abstraction serves three purposes: + *

    + *
  • Transparent handling of different types of sources, such as {@code byte[]}, + * {@link java.nio.ByteBuffer}, {@link java.io.RandomAccessFile}, memory-mapped file.
  • + *
  • Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer} + * may have worked as the unifying abstraction.
  • + *
  • Support sources which do not fit into logical memory as a contiguous region.
  • + *
+ * + *

There are following ways to obtain a chunk of data from the data source: + *

    + *
  • Stream the chunk's data into a {@link DataSink} using + * {@link #feed(long, long, DataSink) feed}. This is best suited for scenarios where there is no + * need to have the chunk's data accessible at the same time, for example, when computing the + * digest of the chunk. If you need to keep the chunk's data around after {@code feed} + * completes, you must create a copy during {@code feed}. However, in that case the following + * methods of obtaining the chunk's data may be more appropriate.
  • + *
  • Obtain a {@link ByteBuffer} containing the chunk's data using + * {@link #getByteBuffer(long, int) getByteBuffer}. Depending on the data source, the chunk's + * data may or may not be copied by this operation. This is best suited for scenarios where + * you need to access the chunk's data in arbitrary order, but don't need to modify the data and + * thus don't require a copy of the data.
  • + *
  • Copy the chunk's data to a {@link ByteBuffer} using + * {@link #copyTo(long, int, ByteBuffer) copyTo}. This is best suited for scenarios where + * you require a copy of the chunk's data, such as to when you need to modify the data. + *
  • + *
+ */ +public interface DataSource { + + /** + * Returns the amount of data (in bytes) contained in this data source. + */ + long size(); + + /** + * Feeds the specified chunk from this data source into the provided sink. + * + * @param offset index (in bytes) at which the chunk starts inside data source + * @param size size (in bytes) of the chunk + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if + * {@code offset + size} is greater than {@link #size()}. + */ + void feed(long offset, long size, DataSink sink) throws IOException; + + /** + * Returns a buffer holding the contents of the specified chunk of data from this data source. + * Changes to the data source are not guaranteed to be reflected in the returned buffer. + * Similarly, changes in the buffer are not guaranteed to be reflected in the data source. + * + *

The returned buffer's position is {@code 0}, and the buffer's limit and capacity is + * {@code size}. + * + * @param offset index (in bytes) at which the chunk starts inside data source + * @param size size (in bytes) of the chunk + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if + * {@code offset + size} is greater than {@link #size()}. + */ + ByteBuffer getByteBuffer(long offset, int size) throws IOException; + + /** + * Copies the specified chunk from this data source into the provided destination buffer, + * advancing the destination buffer's position by {@code size}. + * + * @param offset index (in bytes) at which the chunk starts inside data source + * @param size size (in bytes) of the chunk + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if + * {@code offset + size} is greater than {@link #size()}. + */ + void copyTo(long offset, int size, ByteBuffer dest) throws IOException; + + /** + * Returns a data source representing the specified region of data of this data source. Changes + * to data represented by this data source will also be visible in the returned data source. + * + * @param offset index (in bytes) at which the region starts inside data source + * @param size size (in bytes) of the region + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if + * {@code offset + size} is greater than {@link #size()}. + */ + DataSource slice(long offset, long size); +} diff --git a/app/src/main/java/com/android/apksig/util/DataSources.java b/app/src/main/java/com/android/apksig/util/DataSources.java new file mode 100644 index 0000000..00b89d7 --- /dev/null +++ b/app/src/main/java/com/android/apksig/util/DataSources.java @@ -0,0 +1,63 @@ +/* + * 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.util; + +import com.android.apksig.internal.util.ByteBufferDataSource; +import com.android.apksig.internal.util.RandomAccessFileDataSource; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; + +/** + * Utility methods for working with {@link DataSource} abstraction. + */ +public abstract class DataSources { + private DataSources() {} + + /** + * Returns a {@link DataSource} backed by the provided {@link ByteBuffer}. The data source + * represents the data contained between the position and limit of the buffer. Changes to the + * buffer's contents will be visible in the data source. + */ + public static DataSource asDataSource(ByteBuffer buffer) { + if (buffer == null) { + throw new NullPointerException(); + } + return new ByteBufferDataSource(buffer); + } + + /** + * Returns a {@link DataSource} backed by the provided {@link RandomAccessFile}. Changes to the + * file, including changes to size of file, will be visible in the data source. + */ + public static DataSource asDataSource(RandomAccessFile file) { + if (file == null) { + throw new NullPointerException(); + } + return new RandomAccessFileDataSource(file); + } + + /** + * Returns a {@link DataSource} backed by the provided region of the {@link RandomAccessFile}. + * Changes to the file will be visible in the data source. + */ + public static DataSource asDataSource(RandomAccessFile file, long offset, long size) { + if (file == null) { + throw new NullPointerException(); + } + return new RandomAccessFileDataSource(file, offset, size); + } +} diff --git a/app/src/main/java/com/android/apksig/util/ReadableDataSink.java b/app/src/main/java/com/android/apksig/util/ReadableDataSink.java new file mode 100644 index 0000000..ffc3e2d --- /dev/null +++ b/app/src/main/java/com/android/apksig/util/ReadableDataSink.java @@ -0,0 +1,24 @@ +/* + * 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.util; + +/** + * {@link DataSink} which exposes all data consumed so far as a {@link DataSource}. This abstraction + * offers append-only write access and random read access. + */ +public interface ReadableDataSink extends DataSink, DataSource { +} diff --git a/app/src/main/java/com/android/apksig/util/RunnablesExecutor.java b/app/src/main/java/com/android/apksig/util/RunnablesExecutor.java new file mode 100644 index 0000000..04ec1d8 --- /dev/null +++ b/app/src/main/java/com/android/apksig/util/RunnablesExecutor.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.util; + +public interface RunnablesExecutor { + RunnablesExecutor SINGLE_THREADED = p -> p.getRunnable().run(); + + void execute(RunnablesProvider provider); +} diff --git a/app/src/main/java/com/android/apksig/util/RunnablesProvider.java b/app/src/main/java/com/android/apksig/util/RunnablesProvider.java new file mode 100644 index 0000000..5b7bad2 --- /dev/null +++ b/app/src/main/java/com/android/apksig/util/RunnablesProvider.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.util; + +public interface RunnablesProvider { + Runnable getRunnable(); +} diff --git a/app/src/main/java/com/android/apksig/zip/ZipFormatException.java b/app/src/main/java/com/android/apksig/zip/ZipFormatException.java new file mode 100644 index 0000000..6116c0d --- /dev/null +++ b/app/src/main/java/com/android/apksig/zip/ZipFormatException.java @@ -0,0 +1,32 @@ +/* + * 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.zip; + +/** + * Indicates that a ZIP archive is not well-formed. + */ +public class ZipFormatException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipFormatException(String message) { + super(message); + } + + public ZipFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/app/src/main/java/com/zane/smapiinstaller/MainApplication.java b/app/src/main/java/com/zane/smapiinstaller/MainApplication.java index 64edc37..e7e4c9c 100644 --- a/app/src/main/java/com/zane/smapiinstaller/MainApplication.java +++ b/app/src/main/java/com/zane/smapiinstaller/MainApplication.java @@ -12,6 +12,7 @@ import com.zane.smapiinstaller.utils.GzipRequestInterceptor; import org.greenrobot.greendao.database.Database; +import androidx.multidex.MultiDex; import lombok.Getter; import okhttp3.OkHttpClient; @@ -37,5 +38,6 @@ public class MainApplication extends Application { protected void attachBaseContext(Context base) { // 国际化适配(绑定语种) super.attachBaseContext(LanguagesManager.attach(base)); + MultiDex.install(this); } } diff --git a/app/src/main/java/com/zane/smapiinstaller/entity/ApkFilesManifest.java b/app/src/main/java/com/zane/smapiinstaller/entity/ApkFilesManifest.java index ad28c76..07a5015 100644 --- a/app/src/main/java/com/zane/smapiinstaller/entity/ApkFilesManifest.java +++ b/app/src/main/java/com/zane/smapiinstaller/entity/ApkFilesManifest.java @@ -1,6 +1,7 @@ package com.zane.smapiinstaller.entity; import java.util.List; +import java.util.Set; import lombok.Data; @@ -21,6 +22,8 @@ public class ApkFilesManifest { * 兼容包基础文件路径 */ private String basePath; + + private Set targetPackageName; /** * 文件清单 */ diff --git a/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java b/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java index d779301..05928ac 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/ApkPatcher.java @@ -10,6 +10,7 @@ import android.os.Build; import android.os.Environment; import android.util.Log; +import com.android.apksig.ApkSigner; import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; @@ -23,7 +24,7 @@ import com.zane.smapiinstaller.entity.ManifestEntry; import com.zane.smapiinstaller.utils.FileUtils; import net.fornwall.apksigner.KeyStoreFileManager; -import net.fornwall.apksigner.ZipSigner; +import net.fornwall.apksigner.ZipAligner; import org.apache.commons.lang3.StringUtils; import org.zeroturnaround.zip.ByteSource; @@ -37,7 +38,9 @@ import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.Deflater; @@ -150,6 +153,7 @@ public class ApkPatcher { */ private byte[] modifyManifest(byte[] bytes, List manifests) { AtomicReference packageName = new AtomicReference<>(); + AtomicLong versionCode = new AtomicLong(); Predicate processLogic = (attr) -> { if (attr.type == NodeVisitor.TYPE_STRING) { String strObj = (String) attr.obj; @@ -183,24 +187,28 @@ public class ApkPatcher { } else if(attr.type == NodeVisitor.TYPE_FIRST_INT) { if(StringUtils.equals(attr.name, ManifestPatchConstants.PATTERN_VERSION_CODE)){ - long versionCode = (int) attr.obj; - Iterables.removeIf(manifests, manifest -> { - if (versionCode < manifest.getMinBuildCode()) { - return true; - } - if (manifest.getMaxBuildCode() != null) { - if (versionCode > manifest.getMaxBuildCode()) { - return true; - } - } - return false; - }); + versionCode.set((int) attr.obj); } } return true; }; try { - return CommonLogic.modifyManifest(bytes, processLogic); + byte[] modifyManifest = CommonLogic.modifyManifest(bytes, processLogic); + Iterables.removeIf(manifests, manifest -> { + if (versionCode.get() < manifest.getMinBuildCode()) { + return true; + } + if (manifest.getMaxBuildCode() != null) { + if (versionCode.get() > manifest.getMaxBuildCode()) { + return true; + } + } + if(manifest.getTargetPackageName() != null && packageName.get() != null && !manifest.getTargetPackageName().contains(packageName.get())) { + return true; + } + return false; + }); + return modifyManifest; }catch (Exception e) { errorMessage.set(e.getLocalizedMessage()); return null; @@ -224,7 +232,16 @@ public class ApkPatcher { String alias = ks.aliases().nextElement(); X509Certificate publicKey = (X509Certificate) ks.getCertificate(alias); PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "android".toCharArray()); - ZipSigner.signZip(publicKey, privateKey, "SHA1withRSA", apkPath, signApkPath); + ApkSigner.SignerConfig signerConfig = new ApkSigner.SignerConfig.Builder("debug", privateKey, Collections.singletonList(publicKey)).build(); + ZipAligner.alignZip(apkPath, signApkPath); + new File(apkPath).delete(); + FileUtils.moveFile(new File(signApkPath), new File(apkPath)); + ApkSigner signer = new ApkSigner.Builder(Collections.singletonList(signerConfig)) + .setInputApk(new File(apkPath)) + .setOutputApk(new File(signApkPath)) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true).build(); + signer.sign(); new File(apkPath).delete(); return signApkPath; } diff --git a/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java b/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java index d645ee2..561936b 100644 --- a/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java +++ b/app/src/main/java/com/zane/smapiinstaller/logic/CommonLogic.java @@ -124,7 +124,15 @@ public class CommonLogic { } } } - Collections.sort(apkFilesManifests, (a, b) -> Long.compare(b.getMinBuildCode(), a.getMinBuildCode())); + Collections.sort(apkFilesManifests, (a, b) -> { + if(a.getTargetPackageName() != null && b.getTargetPackageName() == null) { + return -1; + } + else if(b.getTargetPackageName() != null){ + return Long.compare(b.getMinBuildCode(), a.getMinBuildCode()); + } + return 1; + }); return apkFilesManifests; } diff --git a/app/src/main/java/net/fornwall/apksigner/Base64.java b/app/src/main/java/net/fornwall/apksigner/Base64.java deleted file mode 100644 index cc51c04..0000000 --- a/app/src/main/java/net/fornwall/apksigner/Base64.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.fornwall.apksigner; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import org.spongycastle.util.encoders.Base64Encoder; - -/** Base64 encoding handling in a portable way across Android and JSE. */ -public class Base64 { - - public static String encode(byte[] data) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - new Base64Encoder().encode(data, 0, data.length, baos); - } catch (IOException e) { - throw new RuntimeException(e); - } - return new String(baos.toByteArray()); - } - -} \ No newline at end of file diff --git a/app/src/main/java/net/fornwall/apksigner/CertCreator.java b/app/src/main/java/net/fornwall/apksigner/CertCreator.java deleted file mode 100644 index 7dd1c47..0000000 --- a/app/src/main/java/net/fornwall/apksigner/CertCreator.java +++ /dev/null @@ -1,158 +0,0 @@ -package net.fornwall.apksigner; - -import java.io.File; -import java.io.IOException; -import java.math.BigInteger; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.SecureRandom; -import java.security.cert.X509Certificate; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Vector; - -import org.spongycastle.asn1.ASN1ObjectIdentifier; -import org.spongycastle.asn1.x500.style.BCStyle; -import org.spongycastle.jce.X509Principal; -import org.spongycastle.x509.X509V3CertificateGenerator; - -/** All methods create self-signed certificates. */ -public class CertCreator { - - /** Helper class for dealing with the distinguished name RDNs. */ - @SuppressWarnings("serial") - public static class DistinguishedNameValues extends LinkedHashMap { - - public DistinguishedNameValues() { - put(BCStyle.C, null); - put(BCStyle.ST, null); - put(BCStyle.L, null); - put(BCStyle.STREET, null); - put(BCStyle.O, null); - put(BCStyle.OU, null); - put(BCStyle.CN, null); - } - - @Override - public String put(ASN1ObjectIdentifier oid, String value) { - if (value != null && value.equals("")) - value = null; - if (containsKey(oid)) - super.put(oid, value); // preserve original ordering - else { - super.put(oid, value); - // String cn = remove(BCStyle.CN); // CN will always be last. - // put(BCStyle.CN,cn); - } - return value; - } - - public void setCountry(String country) { - put(BCStyle.C, country); - } - - public void setState(String state) { - put(BCStyle.ST, state); - } - - public void setLocality(String locality) { - put(BCStyle.L, locality); - } - - public void setStreet(String street) { - put(BCStyle.STREET, street); - } - - public void setOrganization(String organization) { - put(BCStyle.O, organization); - } - - public void setOrganizationalUnit(String organizationalUnit) { - put(BCStyle.OU, organizationalUnit); - } - - public void setCommonName(String commonName) { - put(BCStyle.CN, commonName); - } - - @Override - public int size() { - int result = 0; - for (String value : values()) { - if (value != null) - result += 1; - } - return result; - } - - public X509Principal getPrincipal() { - Vector oids = new Vector<>(); - Vector values = new Vector<>(); - for (Map.Entry entry : entrySet()) { - if (entry.getValue() != null && !entry.getValue().equals("")) { - oids.add(entry.getKey()); - values.add(entry.getValue()); - } - } - return new X509Principal(oids, values); - } - } - - public static KeySet createKeystoreAndKey(String storePath, char[] storePass, String keyAlgorithm, int keySize, - String keyName, char[] keyPass, String certSignatureAlgorithm, int certValidityYears, - DistinguishedNameValues distinguishedNameValues) { - try { - KeySet keySet = createKey(keyAlgorithm, keySize, certSignatureAlgorithm, certValidityYears, - distinguishedNameValues); - - KeyStore privateKS = KeyStoreFileManager.createKeyStore(storePass); - privateKS.setKeyEntry(keyName, keySet.privateKey, keyPass, - new java.security.cert.Certificate[] { keySet.publicKey }); - - File sfile = new File(storePath); - if (sfile.exists()) { - throw new IOException("File already exists: " + storePath); - } - KeyStoreFileManager.writeKeyStore(privateKS, storePath, storePass); - - return keySet; - } catch (RuntimeException x) { - throw x; - } catch (Exception x) { - throw new RuntimeException(x.getMessage(), x); - } - } - - private static KeySet createKey(String keyAlgorithm, int keySize, String certSignatureAlgorithm, - int certValidityYears, DistinguishedNameValues distinguishedNameValues) { - try { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyAlgorithm); - keyPairGenerator.initialize(keySize); - KeyPair KPair = keyPairGenerator.generateKeyPair(); - - X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator(); - X509Principal principal = distinguishedNameValues.getPrincipal(); - - // generate a positive serial number - BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt()); - while (serialNumber.compareTo(BigInteger.ZERO) < 0) - serialNumber = BigInteger.valueOf(new SecureRandom().nextInt()); - v3CertGen.setSerialNumber(serialNumber); - v3CertGen.setIssuerDN(principal); - v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L)); - v3CertGen.setNotAfter( - new Date(System.currentTimeMillis() + (1000L * 60L * 60L * 24L * 366L * certValidityYears))); - v3CertGen.setSubjectDN(principal); - v3CertGen.setPublicKey(KPair.getPublic()); - v3CertGen.setSignatureAlgorithm(certSignatureAlgorithm); - - X509Certificate PKCertificate = v3CertGen.generate(KPair.getPrivate(), - KeyStoreFileManager.SECURITY_PROVIDER.getName()); - return new KeySet(PKCertificate, KPair.getPrivate(), null); - } catch (Exception x) { - throw new RuntimeException(x.getMessage(), x); - } - } -} diff --git a/app/src/main/java/net/fornwall/apksigner/KeySet.java b/app/src/main/java/net/fornwall/apksigner/KeySet.java deleted file mode 100644 index ec32b71..0000000 --- a/app/src/main/java/net/fornwall/apksigner/KeySet.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.fornwall.apksigner; - -import java.security.PrivateKey; -import java.security.cert.X509Certificate; - -public class KeySet { - - /** Certificate. */ - public final X509Certificate publicKey; - /** Private key. */ - public final PrivateKey privateKey; - public final String signatureAlgorithm; - - public KeySet(X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm) { - this.publicKey = publicKey; - this.privateKey = privateKey; - this.signatureAlgorithm = (signatureAlgorithm != null) ? signatureAlgorithm : "SHA1withRSA"; - } - -} diff --git a/app/src/main/java/net/fornwall/apksigner/SignatureBlockGenerator.java b/app/src/main/java/net/fornwall/apksigner/SignatureBlockGenerator.java deleted file mode 100644 index c36933d..0000000 --- a/app/src/main/java/net/fornwall/apksigner/SignatureBlockGenerator.java +++ /dev/null @@ -1,56 +0,0 @@ -package net.fornwall.apksigner; - -import org.spongycastle.cert.jcajce.JcaCertStore; -import org.spongycastle.cms.*; -import org.spongycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; -import org.spongycastle.operator.ContentSigner; -import org.spongycastle.operator.DigestCalculatorProvider; -import org.spongycastle.operator.jcajce.JcaContentSignerBuilder; -import org.spongycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; -import org.spongycastle.util.Store; - -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; - -public class SignatureBlockGenerator { - - /** - * Sign the given content using the private and public keys from the keySet and return the encoded CMS (PKCS#7) - * data. Use of direct signature and DER encoding produces a block that is verifiable by Android recovery programs. - */ - public static byte[] generate(KeySet keySet, byte[] content) { - try { - List certList = new ArrayList<>(); - CMSTypedData msg = new CMSProcessableByteArray(content); - - certList.add(keySet.publicKey); - - Store certs = new JcaCertStore(certList); - - CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); - - JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(keySet.signatureAlgorithm) - .setProvider("SC"); - ContentSigner sha1Signer = jcaContentSignerBuilder.build(keySet.privateKey); - - JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder() - .setProvider("SC"); - DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build(); - - JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder( - digestCalculatorProvider); - jcaSignerInfoGeneratorBuilder.setDirectSignature(true); - SignerInfoGenerator signerInfoGenerator = jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.publicKey); - - gen.addSignerInfoGenerator(signerInfoGenerator); - gen.addCertificates(certs); - - CMSSignedData sigData = gen.generate(msg, false); - return sigData.toASN1Structure().getEncoded("DER"); - } catch (Exception x) { - throw new RuntimeException(x.getMessage(), x); - } - } - -} diff --git a/app/src/main/java/net/fornwall/apksigner/ZipAligner.java b/app/src/main/java/net/fornwall/apksigner/ZipAligner.java new file mode 100644 index 0000000..62892f2 --- /dev/null +++ b/app/src/main/java/net/fornwall/apksigner/ZipAligner.java @@ -0,0 +1,29 @@ +package net.fornwall.apksigner; + +import net.fornwall.apksigner.zipio.ZioEntry; +import net.fornwall.apksigner.zipio.ZipInput; +import net.fornwall.apksigner.zipio.ZipOutput; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; + +public class ZipAligner { + public static void alignZip(String inputZipFilename, String outputZipFilename) throws IOException, GeneralSecurityException { + File inFile = new File(inputZipFilename).getCanonicalFile(); + File outFile = new File(outputZipFilename).getCanonicalFile(); + if (inFile.equals(outFile)) { + throw new IllegalArgumentException("Input and output files are the same"); + } + + try (ZipInput input = new ZipInput(inputZipFilename)) { + try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) { + for (ZioEntry inEntry : input.entries.values()) { + zipOutput.write(inEntry); + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/fornwall/apksigner/ZipSigner.java b/app/src/main/java/net/fornwall/apksigner/ZipSigner.java deleted file mode 100644 index 1a720bc..0000000 --- a/app/src/main/java/net/fornwall/apksigner/ZipSigner.java +++ /dev/null @@ -1,186 +0,0 @@ -package net.fornwall.apksigner; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.security.DigestOutputStream; -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.jar.Attributes; -import java.util.jar.JarFile; -import java.util.jar.Manifest; -import java.util.regex.Pattern; - -import net.fornwall.apksigner.zipio.ZioEntry; -import net.fornwall.apksigner.zipio.ZipInput; -import net.fornwall.apksigner.zipio.ZipOutput; - -/** - * This is a modified copy of com.android.signapk.SignApk.java. It provides an API to sign JAR files (including APKs and - * Zip/OTA updates) in a way compatible with the mincrypt verifier, using SHA1 and RSA keys. - */ -public class ZipSigner { - - static { - if (!KeyStoreFileManager.SECURITY_PROVIDER.getName().equals("SC")) { - throw new RuntimeException("Invalid security provider"); - } - } - - private static final String CERT_SF_NAME = "META-INF/CERT.SF"; - private static final String CERT_RSA_NAME = "META-INF/CERT.RSA"; - - // Files matching this pattern are not copied to the output. - private static final Pattern stripPattern = Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$"); - - /** Add the SHA1 of every file to the manifest, creating it if necessary. */ - private static Manifest addDigestsToManifest(Map entries) - throws IOException, GeneralSecurityException { - Manifest input = null; - ZioEntry manifestEntry = entries.get(JarFile.MANIFEST_NAME); - if (manifestEntry != null) { - input = new Manifest(); - input.read(manifestEntry.getInputStream()); - } - Manifest output = new Manifest(); - Attributes main = output.getMainAttributes(); - if (input != null) { - main.putAll(input.getMainAttributes()); - } else { - main.putValue("Manifest-Version", "1.0"); - main.putValue("Created-By", "1.0 (Android SignApk)"); - } - - MessageDigest md = MessageDigest.getInstance("SHA1"); - byte[] buffer = new byte[512]; - int num; - - // We sort the input entries by name, and add them to the output manifest in sorted order. We expect that the - // output map will be deterministic. - TreeMap byName = new TreeMap<>(); - byName.putAll(entries); - - // if (debug) getLogger().debug("Manifest entries:"); - for (ZioEntry entry : byName.values()) { - String name = entry.getName(); - // if (debug) getLogger().debug(name); - if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) && !name.equals(CERT_SF_NAME) - && !name.equals(CERT_RSA_NAME) && (stripPattern == null || !stripPattern.matcher(name).matches())) { - - InputStream data = entry.getInputStream(); - while ((num = data.read(buffer)) > 0) { - md.update(buffer, 0, num); - } - - Attributes attr = null; - if (input != null) { - java.util.jar.Attributes inAttr = input.getAttributes(name); - if (inAttr != null) - attr = new Attributes(inAttr); - } - if (attr == null) - attr = new Attributes(); - attr.putValue("SHA1-Digest", Base64.encode(md.digest())); - output.getEntries().put(name, attr); - } - } - - return output; - } - - /** Write the signature file to the given output stream. */ - private static byte[] generateSignatureFile(Manifest manifest) throws IOException, GeneralSecurityException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(("Signature-Version: 1.0\r\n").getBytes()); - out.write(("Created-By: 1.0 (Android SignApk)\r\n").getBytes()); - - MessageDigest md = MessageDigest.getInstance("SHA1"); - PrintStream print = new PrintStream(new DigestOutputStream(new ByteArrayOutputStream(), md), true, "UTF-8"); - - // Digest of the entire manifest - manifest.write(print); - print.flush(); - - out.write(("SHA1-Digest-Manifest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes()); - - Map entries = manifest.getEntries(); - for (Map.Entry entry : entries.entrySet()) { - // Digest of the manifest stanza for this entry. - String nameEntry = "Name: " + entry.getKey() + "\r\n"; - print.print(nameEntry); - for (Map.Entry att : entry.getValue().entrySet()) { - print.print(att.getKey() + ": " + att.getValue() + "\r\n"); - } - print.print("\r\n"); - print.flush(); - - out.write(nameEntry.getBytes()); - out.write(("SHA1-Digest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes()); - } - return out.toByteArray(); - } - - /** - * Sign the file using the given public key cert, private key, and signature block template. The signature block - * template parameter may be null, but if so android-sun-jarsign-support.jar must be in the classpath. - */ - public static void signZip(X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm, - String inputZipFilename, String outputZipFilename) throws IOException, GeneralSecurityException { - KeySet keySet = new KeySet(publicKey, privateKey, signatureAlgorithm); - - File inFile = new File(inputZipFilename).getCanonicalFile(); - File outFile = new File(outputZipFilename).getCanonicalFile(); - if (inFile.equals(outFile)) - throw new IllegalArgumentException("Input and output files are the same"); - - try (ZipInput input = new ZipInput(inputZipFilename)) { - try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) { - // Assume the certificate is valid for at least an hour. - long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; - - // MANIFEST.MF - Manifest manifest = addDigestsToManifest(input.entries); - ZioEntry ze = new ZioEntry(JarFile.MANIFEST_NAME); - ze.setTime(timestamp); - manifest.write(ze.getOutputStream()); - zipOutput.write(ze); - - byte[] certSfBytes = generateSignatureFile(manifest); - - // CERT.SF - ze = new ZioEntry(CERT_SF_NAME); - ze.setTime(timestamp); - ze.getOutputStream().write(certSfBytes); - zipOutput.write(ze); - - // CERT.RSA - ze = new ZioEntry(CERT_RSA_NAME); - ze.setTime(timestamp); - ze.getOutputStream().write(SignatureBlockGenerator.generate(keySet, certSfBytes)); - zipOutput.write(ze); - - // Copy all the files in a manifest from input to output. We set the modification times in the output to - // a fixed time, so as to reduce variation in the output file and make incremental OTAs more efficient. - Map entries = manifest.getEntries(); - List names = new ArrayList<>(entries.keySet()); - Collections.sort(names); - for (String name : names) { - ZioEntry inEntry = input.entries.get(name); - inEntry.setTime(timestamp); - zipOutput.write(inEntry); - } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c79a2f7..e49d6f3 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -61,7 +61,7 @@ Registro detallado Firmando paquete deinstalación SMAPI Stardew Valley - Versión SMAPI: 3.3.2.4 + Versión SMAPI: 3.4.0 Nota: Requiere la versión del juego 1.4.5.138 o superior El cuerpo del juego debe instalarse durante la actualización o instalación Desempacando diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2730926..878f238 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -61,7 +61,7 @@ Journalisation détaillée Signature SMAPI Stardew Valley - Version SMAPI: 3.3.2.4 + Version SMAPI: 3.4.0 Remarques: La version du jeu 1.4.5.138 ou ultérieure est requise. Le jeu de base est requis lors de la mise à jour / installation. Déballage diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 636be0d..f479faf 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -61,7 +61,7 @@ Catatan Terperinci Menandatangani SMAPI Stardew Valley - Versi SMAPI: 3.3.2.4 + Versi SMAPI: 3.4.0 Catatan: Dibutuhkan Stardew Valley versi 1.4.5.138 atau yang lebih baru. Permainan dasar diperlukan saat memperbarui/menginstal. Membongkar diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index edae6b0..77b62cf 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -61,7 +61,7 @@ 자세한 로그 설치 패키지 서명 SMAPI Stardew Valley - SMAPI버전: 3.3.2.4 + SMAPI버전: 3.4.0 참고 : 게임 버전 1.4.5.138 이상이 필요합니다 업데이트 또는 설치 중에 게임 본체를 설치해야합니다 포장 풀기 diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 273b903..bcb877d 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -61,7 +61,7 @@ Log detalhado Assinatura SMAPI Stardew Valley - Versão SMAPI: 3.3.2.4 + Versão SMAPI: 3.4.0 Notas: É necessária a versão do jogo 1.4.5.138 ou posterior. O jogo base é necessário ao atualizar / instalar. Desembalar diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index ef99ea6..a888b22 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -61,7 +61,7 @@ การบันทึกอย่างละเอียด การลงชื่อ SMAPI Stardew Valley - เวอร์ชั่น SMAPI: 3.3.2.4 + เวอร์ชั่น SMAPI: 3.4.0 หมายเหตุ: จำเป็นต้องใช้เวอร์ชั่นเกม 1.4.5.138 หรือใหม่กว่า จำเป็นต้องมีเกมพื้นฐานเมื่อทำการอัพเดต / ติดตั้ง แกะกล่อง diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 383c3f8..5fcb289 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -60,7 +60,7 @@ 詳細日誌 正在簽名安裝包 SMAPI星露穀物語 - SMAPI版本: 3.3.2.4 + SMAPI版本: 3.4.0 注意:需要不低於1.4.5.138版本的遊戲本體 更新或安裝期間需要安裝遊戲本體 正在解包 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 383c3f8..5fcb289 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -60,7 +60,7 @@ 詳細日誌 正在簽名安裝包 SMAPI星露穀物語 - SMAPI版本: 3.3.2.4 + SMAPI版本: 3.4.0 注意:需要不低於1.4.5.138版本的遊戲本體 更新或安裝期間需要安裝遊戲本體 正在解包 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index d0581be..429991a 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -60,7 +60,7 @@ 详细日志 正在签名安装包 SMAPI星露谷物语 - SMAPI版本: 3.3.2.4 + SMAPI版本: 3.4.0 注意:需要不低于1.4.5.138版本的游戏本体 更新或安装期间需要安装游戏本体 正在解包 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 510cda8..47862c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,7 +60,7 @@ Verbose Logging Signing SMAPI Stardew Valley - SMAPI Version: 3.3.2.4 + SMAPI Version: 3.4.0 Notes: Game version 1.4.5.138 or later is required. The base game is required when updating/installing. Unpacking