summaryrefslogtreecommitdiffstats
path: root/platform
diff options
context:
space:
mode:
Diffstat (limited to 'platform')
-rw-r--r--platform/android/SCsub31
-rw-r--r--platform/android/audio_driver_opensl.cpp35
-rw-r--r--platform/android/audio_driver_opensl.h18
-rw-r--r--platform/android/detect.py2
-rw-r--r--platform/android/dir_access_jandroid.cpp26
-rw-r--r--platform/android/dir_access_jandroid.h2
-rw-r--r--platform/android/display_server_android.cpp1
-rw-r--r--platform/android/export/export.cpp11
-rw-r--r--platform/android/export/export_plugin.cpp109
-rw-r--r--platform/android/file_access_android.h2
-rw-r--r--platform/android/file_access_filesystem_jandroid.cpp26
-rw-r--r--platform/android/file_access_filesystem_jandroid.h2
-rw-r--r--platform/android/java/THIRDPARTY.md (renamed from platform/android/java/lib/THIRDPARTY.md)28
-rw-r--r--platform/android/java/app/AndroidManifest.xml4
-rw-r--r--platform/android/java/app/build.gradle68
-rw-r--r--platform/android/java/app/config.gradle18
-rw-r--r--platform/android/java/build.gradle234
-rw-r--r--platform/android/java/editor/build.gradle3
-rw-r--r--platform/android/java/editor/src/main/assets/keystores/debug.keystorebin0 -> 2714 bytes
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/ApkSigner.java1801
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/ApkSignerEngine.java550
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/ApkVerificationIssue.java173
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/ApkVerifier.java3657
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/Constants.java65
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/DefaultApkSignerEngine.java2241
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/Hints.java123
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/README.md32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/SigningCertificateLineage.java1325
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/SourceStampVerifier.java911
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkFormatException.java35
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtils.java670
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtilsLite.java199
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java46
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/MinSdkVersionException.java40
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java869
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java104
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java104
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java1444
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java393
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java35
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java61
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java27
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java225
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java53
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java30
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java235
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java364
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java109
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java139
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java286
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java159
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java74
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java26
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java586
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java1570
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java25
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java329
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java471
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java66
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java531
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java783
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java314
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java440
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java267
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java311
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java673
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java28
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java596
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java45
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java38
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java30
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java23
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java35
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java115
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java225
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java208
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java313
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestParser.java363
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java127
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java61
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/oid/OidConstants.java463
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java173
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java36
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java36
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java46
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java43
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java29
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java58
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java42
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java61
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java74
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java240
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java125
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java59
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java33
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteStreams.java41
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java145
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java219
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java191
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java68
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java89
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java51
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java77
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/Pair.java81
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java104
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/TeeDataSink.java51
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java325
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java282
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java35
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Certificate.java105
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Extension.java38
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Name.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RSAPublicKey.java35
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java33
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java36
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java79
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Time.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Validity.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java304
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/EocdRecord.java57
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java543
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/ZipUtils.java385
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/DataSink.java46
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/DataSinks.java75
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/DataSource.java110
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/DataSources.java80
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/ReadableDataSink.java24
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesExecutor.java61
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesProvider.java21
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipFormatException.java32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipSections.java85
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt28
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt6
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/utils/ApkSignerUtil.kt204
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.kt318
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt6
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java33
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java16
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotHost.java28
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotIO.java2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotLib.java11
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java7
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java15
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/error/Error.kt100
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java69
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java10
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt41
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java241
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java18
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java353
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt29
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt47
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt186
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt6
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt151
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt220
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt65
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt16
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt4
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java4
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt16
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt15
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt8
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt6
-rw-r--r--platform/android/java_godot_lib_jni.cpp83
-rw-r--r--platform/android/java_godot_lib_jni.h2
-rw-r--r--platform/android/java_godot_wrapper.cpp73
-rw-r--r--platform/android/java_godot_wrapper.h8
-rw-r--r--platform/android/os_android.cpp10
-rw-r--r--platform/android/os_android.h5
-rw-r--r--platform/android/rendering_context_driver_vulkan_android.cpp2
-rw-r--r--platform/ios/detect.py19
-rw-r--r--platform/ios/display_server_ios.h4
-rw-r--r--platform/ios/display_server_ios.mm26
-rw-r--r--platform/ios/doc_classes/EditorExportPlatformIOS.xml6
-rw-r--r--platform/ios/export/export_plugin.cpp11
-rw-r--r--platform/ios/godot_app_delegate.h2
-rw-r--r--platform/ios/godot_app_delegate.m4
-rw-r--r--platform/ios/godot_view.mm6
-rw-r--r--platform/ios/keyboard_input_view.mm30
-rw-r--r--platform/ios/main.m2
-rw-r--r--platform/ios/rendering_context_driver_vulkan_ios.mm2
-rw-r--r--platform/linuxbsd/detect.py2
-rw-r--r--platform/linuxbsd/export/export_plugin.cpp100
-rw-r--r--platform/linuxbsd/export/export_plugin.h2
-rw-r--r--platform/linuxbsd/joypad_linux.cpp6
-rw-r--r--platform/linuxbsd/joypad_linux.h15
-rw-r--r--platform/linuxbsd/wayland/display_server_wayland.cpp2
-rw-r--r--platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp2
-rw-r--r--platform/linuxbsd/wayland/wayland_thread.cpp95
-rw-r--r--platform/linuxbsd/wayland/wayland_thread.h10
-rw-r--r--platform/linuxbsd/x11/display_server_x11.cpp37
-rw-r--r--platform/linuxbsd/x11/display_server_x11.h2
-rw-r--r--platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp2
-rw-r--r--platform/macos/SCsub12
-rw-r--r--platform/macos/detect.py26
-rw-r--r--platform/macos/display_server_macos.h5
-rw-r--r--platform/macos/display_server_macos.mm52
-rw-r--r--platform/macos/doc_classes/EditorExportPlatformMacOS.xml6
-rw-r--r--platform/macos/export/export_plugin.cpp78
-rw-r--r--platform/macos/export/export_plugin.h8
-rw-r--r--platform/macos/gl_manager_macos_legacy.h1
-rw-r--r--platform/macos/gl_manager_macos_legacy.mm20
-rw-r--r--platform/macos/godot_content_view.mm3
-rw-r--r--platform/macos/godot_main_macos.mm4
-rw-r--r--platform/macos/godot_menu_item.h1
-rw-r--r--platform/macos/godot_menu_item.mm14
-rw-r--r--platform/macos/joypad_macos.mm5
-rw-r--r--platform/macos/native_menu_macos.mm37
-rw-r--r--platform/macos/os_macos.h1
-rw-r--r--platform/macos/os_macos.mm9
-rw-r--r--platform/macos/rendering_context_driver_vulkan_macos.mm2
-rw-r--r--platform/web/audio_driver_web.cpp26
-rw-r--r--platform/web/audio_driver_web.h2
-rw-r--r--platform/web/detect.py6
-rw-r--r--platform/web/display_server_web.cpp6
-rw-r--r--platform/web/emscripten_helpers.py3
-rw-r--r--platform/web/export/export.cpp2
-rw-r--r--platform/web/export/export_plugin.cpp2
-rw-r--r--platform/web/godot_audio.h4
-rw-r--r--platform/web/http_client_web.cpp6
-rw-r--r--platform/web/http_client_web.h2
-rw-r--r--platform/web/js/engine/config.js2
-rw-r--r--platform/web/js/libs/audio.position.worklet.js (renamed from platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt)35
-rw-r--r--platform/web/js/libs/library_godot_audio.js176
-rw-r--r--platform/web/js/libs/library_godot_input.js1
-rw-r--r--platform/web/js/libs/library_godot_javascript_singleton.js16
-rwxr-xr-xplatform/web/serve.py20
-rw-r--r--platform/web/web_main.cpp4
-rw-r--r--platform/windows/SCsub12
-rw-r--r--platform/windows/detect.py21
-rw-r--r--platform/windows/display_server_windows.cpp851
-rw-r--r--platform/windows/display_server_windows.h116
-rw-r--r--platform/windows/doc_classes/EditorExportPlatformWindows.xml2
-rw-r--r--platform/windows/export/export_plugin.cpp76
-rw-r--r--platform/windows/export/export_plugin.h2
-rw-r--r--platform/windows/gl_manager_windows_angle.cpp5
-rw-r--r--platform/windows/gl_manager_windows_angle.h2
-rw-r--r--platform/windows/gl_manager_windows_native.cpp43
-rw-r--r--platform/windows/gl_manager_windows_native.h2
-rw-r--r--platform/windows/native_menu_windows.cpp37
-rw-r--r--platform/windows/native_menu_windows.h1
-rw-r--r--platform/windows/os_windows.cpp126
-rw-r--r--platform/windows/os_windows.h1
-rw-r--r--platform/windows/rendering_context_driver_vulkan_windows.cpp2
253 files changed, 34865 insertions, 1438 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub
index bc1b5e9200..8c88b419b3 100644
--- a/platform/android/SCsub
+++ b/platform/android/SCsub
@@ -95,25 +95,18 @@ if lib_arch_dir != "":
else:
gradle_process = ["./gradlew"]
- if env["target"] != "editor" and env["dev_build"]:
- subprocess.run(
- gradle_process
- + [
- "generateDevTemplate",
- "--quiet",
- ],
- cwd="platform/android/java",
- )
- else:
- # Android editor with `dev_build=yes` is handled by the `generateGodotEditor` task.
- subprocess.run(
- gradle_process
- + [
- "generateGodotEditor" if env["target"] == "editor" else "generateGodotTemplates",
- "--quiet",
- ],
- cwd="platform/android/java",
- )
+ gradle_process += [
+ "generateGodotEditor" if env["target"] == "editor" else "generateGodotTemplates",
+ "--quiet",
+ ]
+
+ if env["debug_symbols"]:
+ gradle_process += ["-PdoNotStrip=true"]
+
+ subprocess.run(
+ gradle_process,
+ cwd="platform/android/java",
+ )
if env["generate_apk"]:
generate_apk_command = env_android.Command("generate_apk", [], generate_apk)
diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp
index 51e89c720d..ef9c51db07 100644
--- a/platform/android/audio_driver_opensl.cpp
+++ b/platform/android/audio_driver_opensl.cpp
@@ -268,6 +268,10 @@ Error AudioDriverOpenSL::init_input_device() {
}
Error AudioDriverOpenSL::input_start() {
+ if (recordItf || recordBufferQueueItf) {
+ return ERR_ALREADY_IN_USE;
+ }
+
if (OS::get_singleton()->request_permission("RECORD_AUDIO")) {
return init_input_device();
}
@@ -277,6 +281,10 @@ Error AudioDriverOpenSL::input_start() {
}
Error AudioDriverOpenSL::input_stop() {
+ if (!recordItf || !recordBufferQueueItf) {
+ return ERR_CANT_OPEN;
+ }
+
SLuint32 state;
SLresult res = (*recordItf)->GetRecordState(recordItf, &state);
ERR_FAIL_COND_V(res != SL_RESULT_SUCCESS, ERR_CANT_OPEN);
@@ -313,13 +321,36 @@ void AudioDriverOpenSL::unlock() {
}
void AudioDriverOpenSL::finish() {
- (*sl)->Destroy(sl);
+ if (recordItf) {
+ (*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_STOPPED);
+ recordItf = nullptr;
+ }
+ if (recorder) {
+ (*recorder)->Destroy(recorder);
+ recorder = nullptr;
+ }
+ if (playItf) {
+ (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_STOPPED);
+ playItf = nullptr;
+ }
+ if (player) {
+ (*player)->Destroy(player);
+ player = nullptr;
+ }
+ if (OutputMix) {
+ (*OutputMix)->Destroy(OutputMix);
+ OutputMix = nullptr;
+ }
+ if (sl) {
+ (*sl)->Destroy(sl);
+ sl = nullptr;
+ }
}
void AudioDriverOpenSL::set_pause(bool p_pause) {
pause = p_pause;
- if (active) {
+ if (active && playItf) {
if (pause) {
(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PAUSED);
} else {
diff --git a/platform/android/audio_driver_opensl.h b/platform/android/audio_driver_opensl.h
index 6ea0f77def..bcd173826a 100644
--- a/platform/android/audio_driver_opensl.h
+++ b/platform/android/audio_driver_opensl.h
@@ -54,15 +54,15 @@ class AudioDriverOpenSL : public AudioDriver {
Vector<int16_t> rec_buffer;
- SLPlayItf playItf;
- SLRecordItf recordItf;
- SLObjectItf sl;
- SLEngineItf EngineItf;
- SLObjectItf OutputMix;
- SLObjectItf player;
- SLObjectItf recorder;
- SLAndroidSimpleBufferQueueItf bufferQueueItf;
- SLAndroidSimpleBufferQueueItf recordBufferQueueItf;
+ SLPlayItf playItf = nullptr;
+ SLRecordItf recordItf = nullptr;
+ SLObjectItf sl = nullptr;
+ SLEngineItf EngineItf = nullptr;
+ SLObjectItf OutputMix = nullptr;
+ SLObjectItf player = nullptr;
+ SLObjectItf recorder = nullptr;
+ SLAndroidSimpleBufferQueueItf bufferQueueItf = nullptr;
+ SLAndroidSimpleBufferQueueItf recordBufferQueueItf = nullptr;
SLDataSource audioSource;
SLDataFormat_PCM pcm;
SLDataSink audioSink;
diff --git a/platform/android/detect.py b/platform/android/detect.py
index 0b182aca90..0a10754e24 100644
--- a/platform/android/detect.py
+++ b/platform/android/detect.py
@@ -190,6 +190,8 @@ def configure(env: "SConsEnvironment"):
env.Append(CCFLAGS=["-mfix-cortex-a53-835769"])
env.Append(CPPDEFINES=["__ARM_ARCH_8A__"])
+ env.Append(CCFLAGS=["-ffp-contract=off"])
+
# Link flags
env.Append(LINKFLAGS="-Wl,--gc-sections -Wl,--no-undefined -Wl,-z,now".split())
diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp
index ab90527bfa..19c18eb96e 100644
--- a/platform/android/dir_access_jandroid.cpp
+++ b/platform/android/dir_access_jandroid.cpp
@@ -68,7 +68,7 @@ String DirAccessJAndroid::get_next() {
if (_dir_next) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, "");
- jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id);
+ jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, id);
if (!str) {
return "";
}
@@ -85,7 +85,7 @@ bool DirAccessJAndroid::current_is_dir() const {
if (_dir_is_dir) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, false);
- return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, get_access_type(), id);
+ return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, id);
} else {
return false;
}
@@ -95,7 +95,7 @@ bool DirAccessJAndroid::current_is_hidden() const {
if (_current_is_hidden) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, false);
- return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, get_access_type(), id);
+ return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, id);
}
return false;
}
@@ -218,7 +218,7 @@ bool DirAccessJAndroid::dir_exists(String p_dir) {
}
}
-Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
+Error DirAccessJAndroid::make_dir(String p_dir) {
// Check if the directory exists already
if (dir_exists(p_dir)) {
return ERR_ALREADY_EXISTS;
@@ -242,8 +242,12 @@ Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
}
}
-Error DirAccessJAndroid::make_dir(String p_dir) {
- return make_dir_recursive(p_dir);
+Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
+ Error err = make_dir(p_dir);
+ if (err != OK && err != ERR_ALREADY_EXISTS) {
+ ERR_FAIL_V_MSG(err, "Could not create directory: " + p_dir);
+ }
+ return OK;
}
Error DirAccessJAndroid::rename(String p_from, String p_to) {
@@ -307,9 +311,9 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) {
cls = (jclass)env->NewGlobalRef(c);
_dir_open = env->GetMethodID(cls, "dirOpen", "(ILjava/lang/String;)I");
- _dir_next = env->GetMethodID(cls, "dirNext", "(II)Ljava/lang/String;");
- _dir_close = env->GetMethodID(cls, "dirClose", "(II)V");
- _dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(II)Z");
+ _dir_next = env->GetMethodID(cls, "dirNext", "(I)Ljava/lang/String;");
+ _dir_close = env->GetMethodID(cls, "dirClose", "(I)V");
+ _dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(I)Z");
_dir_exists = env->GetMethodID(cls, "dirExists", "(ILjava/lang/String;)Z");
_file_exists = env->GetMethodID(cls, "fileExists", "(ILjava/lang/String;)Z");
_get_drive_count = env->GetMethodID(cls, "getDriveCount", "(I)I");
@@ -318,7 +322,7 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) {
_get_space_left = env->GetMethodID(cls, "getSpaceLeft", "(I)J");
_rename = env->GetMethodID(cls, "rename", "(ILjava/lang/String;Ljava/lang/String;)Z");
_remove = env->GetMethodID(cls, "remove", "(ILjava/lang/String;)Z");
- _current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(II)Z");
+ _current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(I)Z");
}
void DirAccessJAndroid::terminate() {
@@ -355,6 +359,6 @@ void DirAccessJAndroid::dir_close(int p_id) {
if (_dir_close) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL(env);
- env->CallVoidMethod(dir_access_handler, _dir_close, get_access_type(), p_id);
+ env->CallVoidMethod(dir_access_handler, _dir_close, p_id);
}
}
diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h
index 68578b0fa9..1d8fe906f3 100644
--- a/platform/android/dir_access_jandroid.h
+++ b/platform/android/dir_access_jandroid.h
@@ -84,7 +84,7 @@ public:
virtual bool is_link(String p_file) override { return false; }
virtual String read_link(String p_file) override { return p_file; }
- virtual Error create_link(String p_source, String p_target) override { return FAILED; }
+ virtual Error create_link(String p_source, String p_target) override { return ERR_UNAVAILABLE; }
virtual uint64_t get_space_left() override;
diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp
index 06b304dcde..8dc0e869d0 100644
--- a/platform/android/display_server_android.cpp
+++ b/platform/android/display_server_android.cpp
@@ -651,7 +651,6 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis
#endif
Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events);
- Input::get_singleton()->set_use_input_buffering(true); // Needed because events will come directly from the UI thread
r_error = OK;
}
diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp
index 6a6d7149ff..3f4624d09c 100644
--- a/platform/android/export/export.cpp
+++ b/platform/android/export/export.cpp
@@ -42,16 +42,17 @@ void register_android_exporter_types() {
}
void register_android_exporter() {
-#ifndef ANDROID_ENABLED
- EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
- EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
- EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
- EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/debug_keystore", EditorPaths::get_singleton()->get_debug_keystore_path());
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"));
EDITOR_DEF("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
EDITOR_DEF("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore_pass", PROPERTY_HINT_PASSWORD));
+
+#ifndef ANDROID_ENABLED
+ EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
+ EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
+ EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
+ EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/force_system_user", false);
EDITOR_DEF("export/android/shutdown_adb_on_exit", true);
diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp
index 479158c91f..0fdaca4839 100644
--- a/platform/android/export/export_plugin.cpp
+++ b/platform/android/export/export_plugin.cpp
@@ -57,6 +57,10 @@
#include "modules/svg/image_loader_svg.h"
#endif
+#ifdef ANDROID_ENABLED
+#include "../os_android.h"
+#endif
+
#include <string.h>
static const char *android_perms[] = {
@@ -441,6 +445,7 @@ void EditorExportPlatformAndroid::_update_preset_status() {
} else {
has_runnable_preset.clear();
}
+ devices_changed.set();
}
#endif
@@ -2322,7 +2327,8 @@ static bool has_valid_keystore_credentials(String &r_error_str, const String &p_
args.push_back(p_password);
args.push_back("-alias");
args.push_back(p_username);
- Error error = OS::get_singleton()->execute("keytool", args, &output, nullptr, true);
+ String keytool_path = EditorExportPlatformAndroid::get_keytool_path();
+ Error error = OS::get_singleton()->execute(keytool_path, args, &output, nullptr, true);
String keytool_error = "keytool error:";
bool valid = output.substr(0, keytool_error.length()) != keytool_error;
@@ -2415,6 +2421,10 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
err += template_err;
}
} else {
+#ifdef ANDROID_ENABLED
+ err += TTR("Gradle build is not supported for the Android editor.") + "\n";
+ valid = false;
+#else
// Validate the custom gradle android source template.
bool android_source_template_valid = false;
const String android_source_template = p_preset->get("gradle_build/android_source_template");
@@ -2437,6 +2447,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
}
valid = installed_android_build_template && !r_missing_templates;
+#endif
}
// Validate the rest of the export configuration.
@@ -2473,6 +2484,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";
}
+#ifndef ANDROID_ENABLED
String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
if (java_sdk_path.is_empty()) {
err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n";
@@ -2545,6 +2557,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
valid = false;
}
}
+#endif
if (!err.is_empty()) {
r_error = err;
@@ -2715,23 +2728,9 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) {
int export_format = int(p_preset->get("gradle_build/export_format"));
- String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK";
- String release_keystore = _get_keystore_path(p_preset, false);
- String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
- String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
- String target_sdk_version = p_preset->get("gradle_build/target_sdk");
- if (!target_sdk_version.is_valid_int()) {
- target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
- }
- String apksigner = get_apksigner_path(target_sdk_version.to_int(), true);
- print_verbose("Starting signing of the " + export_label + " binary using " + apksigner);
- if (apksigner == "<FAILED>") {
- add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting %s is unsigned."), export_label));
- return OK;
- }
- if (!FileAccess::exists(apksigner)) {
- add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting %s is unsigned."), export_label));
- return OK;
+ if (export_format == EXPORT_FORMAT_AAB) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("AAB signing is not supported"));
+ return FAILED;
}
String keystore;
@@ -2748,15 +2747,15 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
user = EDITOR_GET("export/android/debug_keystore_user");
}
- if (ep.step(vformat(TTR("Signing debug %s..."), export_label), 104)) {
+ if (ep.step(TTR("Signing debug APK..."), 104)) {
return ERR_SKIP;
}
} else {
- keystore = release_keystore;
- password = release_password;
- user = release_username;
+ keystore = _get_keystore_path(p_preset, false);
+ password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
+ user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
- if (ep.step(vformat(TTR("Signing release %s..."), export_label), 104)) {
+ if (ep.step(TTR("Signing release APK..."), 104)) {
return ERR_SKIP;
}
}
@@ -2766,6 +2765,36 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
return ERR_FILE_CANT_OPEN;
}
+ String apk_path = export_path;
+ if (apk_path.is_relative_path()) {
+ apk_path = OS::get_singleton()->get_resource_dir().path_join(apk_path);
+ }
+ apk_path = ProjectSettings::get_singleton()->globalize_path(apk_path).simplify_path();
+
+ Error err;
+#ifdef ANDROID_ENABLED
+ err = OS_Android::get_singleton()->sign_apk(apk_path, apk_path, keystore, user, password);
+ if (err != OK) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to sign apk."));
+ return err;
+ }
+#else
+ String target_sdk_version = p_preset->get("gradle_build/target_sdk");
+ if (!target_sdk_version.is_valid_int()) {
+ target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
+ }
+
+ String apksigner = get_apksigner_path(target_sdk_version.to_int(), true);
+ print_verbose("Starting signing of the APK binary using " + apksigner);
+ if (apksigner == "<FAILED>") {
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting APK is unsigned."));
+ return OK;
+ }
+ if (!FileAccess::exists(apksigner)) {
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting APK is unsigned."));
+ return OK;
+ }
+
String output;
List<String> args;
args.push_back("sign");
@@ -2776,7 +2805,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
args.push_back("pass:" + password);
args.push_back("--ks-key-alias");
args.push_back(user);
- args.push_back(export_path);
+ args.push_back(apk_path);
if (OS::get_singleton()->is_stdout_verbose() && p_debug) {
// We only print verbose logs with credentials for debug builds to avoid leaking release keystore credentials.
print_verbose("Signing debug binary using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
@@ -2788,7 +2817,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
print_line("Signing binary using: " + String("\n") + apksigner + " " + join_list(redacted_args, String(" ")));
}
int retval;
- Error err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
+ err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
return err;
@@ -2800,15 +2829,23 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
return ERR_CANT_CREATE;
}
+#endif
- if (ep.step(vformat(TTR("Verifying %s..."), export_label), 105)) {
+ if (ep.step(TTR("Verifying APK..."), 105)) {
return ERR_SKIP;
}
+#ifdef ANDROID_ENABLED
+ err = OS_Android::get_singleton()->verify_apk(apk_path);
+ if (err != OK) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to verify signed apk."));
+ return err;
+ }
+#else
args.clear();
args.push_back("verify");
args.push_back("--verbose");
- args.push_back(export_path);
+ args.push_back(apk_path);
if (p_debug) {
print_verbose("Verifying signed build using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
}
@@ -2821,10 +2858,11 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
}
print_verbose(output);
if (retval) {
- add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' verification of %s failed."), export_label));
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' verification of APK failed."));
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
return ERR_CANT_CREATE;
}
+#endif
print_verbose("Successfully completed signing build.");
return OK;
@@ -3268,18 +3306,17 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
}
List<String> copy_args;
- String copy_command;
- if (export_format == EXPORT_FORMAT_AAB) {
- copy_command = vformat("copyAndRename%sAab", build_type);
- } else if (export_format == EXPORT_FORMAT_APK) {
- copy_command = vformat("copyAndRename%sApk", build_type);
- }
-
+ String copy_command = "copyAndRenameBinary";
copy_args.push_back(copy_command);
copy_args.push_back("-p"); // argument to specify the start directory.
copy_args.push_back(build_path); // start directory.
+ copy_args.push_back("-Pexport_build_type=" + build_type.to_lower());
+
+ String export_format_arg = export_format == EXPORT_FORMAT_AAB ? "aab" : "apk";
+ copy_args.push_back("-Pexport_format=" + export_format_arg);
+
String export_filename = p_path.get_file();
String export_path = p_path.get_base_dir();
if (export_path.is_relative_path()) {
@@ -3318,7 +3355,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
src_apk = find_export_template("android_release.apk");
}
if (src_apk.is_empty()) {
- add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Package not found: \"%s\"."), src_apk));
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("%s export template not found: \"%s\"."), (p_debug ? "Debug" : "Release"), src_apk));
return ERR_FILE_NOT_FOUND;
}
}
diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h
index e79daeafb3..b465a92c78 100644
--- a/platform/android/file_access_android.h
+++ b/platform/android/file_access_android.h
@@ -86,7 +86,7 @@ public:
virtual uint64_t _get_modified_time(const String &p_file) override { return 0; }
virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; }
- virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; }
+ virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return ERR_UNAVAILABLE; }
virtual bool _get_hidden_attribute(const String &p_file) override { return false; }
virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; }
diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp
index f28d469d07..9ae48dfb10 100644
--- a/platform/android/file_access_filesystem_jandroid.cpp
+++ b/platform/android/file_access_filesystem_jandroid.cpp
@@ -77,15 +77,9 @@ Error FileAccessFilesystemJAndroid::open_internal(const String &p_path, int p_mo
int res = env->CallIntMethod(file_access_handler, _file_open, js, p_mode_flags);
env->DeleteLocalRef(js);
- if (res <= 0) {
- switch (res) {
- case 0:
- default:
- return ERR_FILE_CANT_OPEN;
-
- case -2:
- return ERR_FILE_NOT_FOUND;
- }
+ if (res < 0) {
+ // Errors are passed back as their negative value to differentiate from the positive file id.
+ return static_cast<Error>(-res);
}
id = res;
@@ -331,19 +325,7 @@ Error FileAccessFilesystemJAndroid::resize(int64_t p_length) {
ERR_FAIL_NULL_V(env, FAILED);
ERR_FAIL_COND_V_MSG(!is_open(), FAILED, "File must be opened before use.");
int res = env->CallIntMethod(file_access_handler, _file_resize, id, p_length);
- switch (res) {
- case 0:
- return OK;
- case -4:
- return ERR_INVALID_PARAMETER;
- case -3:
- return ERR_FILE_CANT_OPEN;
- case -2:
- return ERR_FILE_NOT_FOUND;
- case -1:
- default:
- return FAILED;
- }
+ return static_cast<Error>(res);
} else {
return ERR_UNAVAILABLE;
}
diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h
index 6a8fc524b7..2795ac02ac 100644
--- a/platform/android/file_access_filesystem_jandroid.h
+++ b/platform/android/file_access_filesystem_jandroid.h
@@ -101,7 +101,7 @@ public:
virtual uint64_t _get_modified_time(const String &p_file) override;
virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; }
- virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; }
+ virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return ERR_UNAVAILABLE; }
virtual bool _get_hidden_attribute(const String &p_file) override { return false; }
virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; }
diff --git a/platform/android/java/lib/THIRDPARTY.md b/platform/android/java/THIRDPARTY.md
index 2496b59263..7807cc55ff 100644
--- a/platform/android/java/lib/THIRDPARTY.md
+++ b/platform/android/java/THIRDPARTY.md
@@ -3,14 +3,6 @@
This file list third-party libraries used in the Android source folder,
with their provenance and, when relevant, modifications made to those files.
-## com.android.vending.billing
-
-- Upstream: https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive/app/src/main
-- Version: git (7a94c69, 2019)
-- License: Apache 2.0
-
-Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`.
-
## com.google.android.vending.expansion.downloader
- Upstream: https://github.com/google/play-apk-expansion/tree/master/apkx_library
@@ -19,10 +11,10 @@ Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`.
Overwrite all files under:
-- `src/com/google/android/vending/expansion/downloader`
+- `lib/src/com/google/android/vending/expansion/downloader`
Some files have been modified for yet unclear reasons.
-See the `patches/com.google.android.vending.expansion.downloader.patch` file.
+See the `lib/patches/com.google.android.vending.expansion.downloader.patch` file.
## com.google.android.vending.licensing
@@ -32,8 +24,18 @@ See the `patches/com.google.android.vending.expansion.downloader.patch` file.
Overwrite all files under:
-- `aidl/com/android/vending/licensing`
-- `src/com/google/android/vending/licensing`
+- `lib/aidl/com/android/vending/licensing`
+- `lib/src/com/google/android/vending/licensing`
Some files have been modified to silence linter errors or fix downstream issues.
-See the `patches/com.google.android.vending.licensing.patch` file.
+See the `lib/patches/com.google.android.vending.licensing.patch` file.
+
+## com.android.apksig
+
+- Upstream: https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888
+- Version: git (ac5cbb07d87cc342fcf07715857a812305d69888, 2024)
+- License: Apache 2.0
+
+Overwrite all files under:
+
+- `editor/src/main/java/com/android/apksig`
diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml
index 4abc6548bf..0cc929d226 100644
--- a/platform/android/java/app/AndroidManifest.xml
+++ b/platform/android/java/app/AndroidManifest.xml
@@ -24,6 +24,10 @@
android:hasFragileUserData="false"
android:requestLegacyExternalStorage="false"
tools:ignore="GoogleAppIndexingWarning" >
+ <profileable
+ android:shell="true"
+ android:enabled="true"
+ tools:targetApi="29" />
<!-- Records the version of the Godot editor used for building -->
<meta-data
diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle
index 01d5d9ef92..05b4f379b3 100644
--- a/platform/android/java/app/build.gradle
+++ b/platform/android/java/app/build.gradle
@@ -211,70 +211,24 @@ android {
}
}
-task copyAndRenameDebugApk(type: Copy) {
+task copyAndRenameBinary(type: Copy) {
// The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
// and directories. Otherwise this check may cause permissions access failures on Windows
// machines.
doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
- from "$buildDir/outputs/apk/debug/android_debug.apk"
- into getExportPath()
- rename "android_debug.apk", getExportFilename()
-}
+ String exportPath = getExportPath()
+ String exportFilename = getExportFilename()
+ String exportBuildType = getExportBuildType()
+ String exportFormat = getExportFormat()
-task copyAndRenameDevApk(type: Copy) {
- // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
- // and directories. Otherwise this check may cause permissions access failures on Windows
- // machines.
- doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
-
- from "$buildDir/outputs/apk/dev/android_dev.apk"
- into getExportPath()
- rename "android_dev.apk", getExportFilename()
-}
-
-task copyAndRenameReleaseApk(type: Copy) {
- // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
- // and directories. Otherwise this check may cause permissions access failures on Windows
- // machines.
- doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
-
- from "$buildDir/outputs/apk/release/android_release.apk"
- into getExportPath()
- rename "android_release.apk", getExportFilename()
-}
-
-task copyAndRenameDebugAab(type: Copy) {
- // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
- // and directories. Otherwise this check may cause permissions access failures on Windows
- // machines.
- doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
-
- from "$buildDir/outputs/bundle/debug/build-debug.aab"
- into getExportPath()
- rename "build-debug.aab", getExportFilename()
-}
-
-task copyAndRenameDevAab(type: Copy) {
- // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
- // and directories. Otherwise this check may cause permissions access failures on Windows
- // machines.
- doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
-
- from "$buildDir/outputs/bundle/dev/build-dev.aab"
- into getExportPath()
- rename "build-dev.aab", getExportFilename()
-}
-
-task copyAndRenameReleaseAab(type: Copy) {
- // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
- // and directories. Otherwise this check may cause permissions access failures on Windows
- // machines.
- doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
+ boolean isAab = exportFormat == "aab"
+ String sourceFilepath = isAab ? "$buildDir/outputs/bundle/$exportBuildType/build-${exportBuildType}.aab" : "$buildDir/outputs/apk/$exportBuildType/android_${exportBuildType}.apk"
+ String sourceFilename = isAab ? "build-${exportBuildType}.aab" : "android_${exportBuildType}.apk"
- from "$buildDir/outputs/bundle/release/build-release.aab"
- into getExportPath()
- rename "build-release.aab", getExportFilename()
+ from sourceFilepath
+ into exportPath
+ rename sourceFilename, exportFilename
}
/**
diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle
index 01759a1b2f..611a9c4a40 100644
--- a/platform/android/java/app/config.gradle
+++ b/platform/android/java/app/config.gradle
@@ -7,7 +7,7 @@ ext.versions = [
targetSdk : 34,
buildTools : '34.0.0',
kotlinVersion : '1.9.20',
- fragmentVersion : '1.6.2',
+ fragmentVersion : '1.7.1',
nexusPublishVersion: '1.3.0',
javaVersion : JavaVersion.VERSION_17,
// Also update 'platform/android/detect.py#get_ndk_version()' when this is updated.
@@ -224,6 +224,22 @@ ext.getExportFilename = {
return exportFilename
}
+ext.getExportBuildType = {
+ String exportBuildType = project.hasProperty("export_build_type") ? project.property("export_build_type") : ""
+ if (exportBuildType == null || exportBuildType.isEmpty()) {
+ exportBuildType = "debug"
+ }
+ return exportBuildType
+}
+
+ext.getExportFormat = {
+ String exportFormat = project.hasProperty("export_format") ? project.property("export_format") : ""
+ if (exportFormat == null || exportFormat.isEmpty()) {
+ exportFormat = "apk"
+ }
+ return exportFormat
+}
+
/**
* Parse the project properties for the 'plugins_maven_repos' property and return the list
* of maven repos.
diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle
index b91b023ce6..771bda6948 100644
--- a/platform/android/java/build.gradle
+++ b/platform/android/java/build.gradle
@@ -35,116 +35,17 @@ ext {
// `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s).
// The {selectedAbis} values must be from the {supportedAbis} values.
selectedAbis = ["arm64"]
-}
-def rootDir = "../../.."
-def binDir = "$rootDir/bin/"
-def androidEditorBuildsDir = "$binDir/android_editor_builds/"
+ rootDir = "../../.."
+ binDir = "$rootDir/bin/"
+ androidEditorBuildsDir = "$binDir/android_editor_builds/"
+}
def getSconsTaskName(String flavor, String buildType, String abi) {
return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize()
}
/**
- * Copy the generated 'android_debug.apk' binary template into the Godot bin directory.
- * Depends on the app build task to ensure the binary is generated prior to copying.
- */
-task copyDebugBinaryToBin(type: Copy) {
- dependsOn ':app:assembleDebug'
- from('app/build/outputs/apk/debug')
- into(binDir)
- include('android_debug.apk')
-}
-
-/**
- * Copy the generated 'android_dev.apk' binary template into the Godot bin directory.
- * Depends on the app build task to ensure the binary is generated prior to copying.
- */
-task copyDevBinaryToBin(type: Copy) {
- dependsOn ':app:assembleDev'
- from('app/build/outputs/apk/dev')
- into(binDir)
- include('android_dev.apk')
-}
-
-/**
- * Copy the generated 'android_release.apk' binary template into the Godot bin directory.
- * Depends on the app build task to ensure the binary is generated prior to copying.
- */
-task copyReleaseBinaryToBin(type: Copy) {
- dependsOn ':app:assembleRelease'
- from('app/build/outputs/apk/release')
- into(binDir)
- include('android_release.apk')
-}
-
-/**
- * Copy the Godot android library archive debug file into the app module debug libs directory.
- * Depends on the library build task to ensure the AAR file is generated prior to copying.
- */
-task copyDebugAARToAppModule(type: Copy) {
- dependsOn ':lib:assembleTemplateDebug'
- from('lib/build/outputs/aar')
- into('app/libs/debug')
- include('godot-lib.template_debug.aar')
-}
-
-/**
- * Copy the Godot android library archive debug file into the root bin directory.
- * Depends on the library build task to ensure the AAR file is generated prior to copying.
- */
-task copyDebugAARToBin(type: Copy) {
- dependsOn ':lib:assembleTemplateDebug'
- from('lib/build/outputs/aar')
- into(binDir)
- include('godot-lib.template_debug.aar')
-}
-
-/**
- * Copy the Godot android library archive dev file into the app module dev libs directory.
- * Depends on the library build task to ensure the AAR file is generated prior to copying.
- */
-task copyDevAARToAppModule(type: Copy) {
- dependsOn ':lib:assembleTemplateDev'
- from('lib/build/outputs/aar')
- into('app/libs/dev')
- include('godot-lib.template_debug.dev.aar')
-}
-
-/**
- * Copy the Godot android library archive dev file into the root bin directory.
- * Depends on the library build task to ensure the AAR file is generated prior to copying.
- */
-task copyDevAARToBin(type: Copy) {
- dependsOn ':lib:assembleTemplateDev'
- from('lib/build/outputs/aar')
- into(binDir)
- include('godot-lib.template_debug.dev.aar')
-}
-
-/**
- * Copy the Godot android library archive release file into the app module release libs directory.
- * Depends on the library build task to ensure the AAR file is generated prior to copying.
- */
-task copyReleaseAARToAppModule(type: Copy) {
- dependsOn ':lib:assembleTemplateRelease'
- from('lib/build/outputs/aar')
- into('app/libs/release')
- include('godot-lib.template_release.aar')
-}
-
-/**
- * Copy the Godot android library archive release file into the root bin directory.
- * Depends on the library build task to ensure the AAR file is generated prior to copying.
- */
-task copyReleaseAARToBin(type: Copy) {
- dependsOn ':lib:assembleTemplateRelease'
- from('lib/build/outputs/aar')
- into(binDir)
- include('godot-lib.template_release.aar')
-}
-
-/**
* Generate Godot gradle build template by zipping the source files from the app directory, as well
* as the AAR files generated by 'copyDebugAAR', 'copyDevAAR' and 'copyReleaseAAR'.
* The zip file also includes some gradle tools to enable gradle builds from the Godot Editor.
@@ -197,7 +98,7 @@ def generateBuildTasks(String flavor = "template") {
throw new GradleException("Invalid build flavor: $flavor")
}
- def tasks = []
+ def buildTasks = []
// Only build the apks and aar files for which we have native shared libraries unless we intend
// to run the scons build tasks.
@@ -206,72 +107,93 @@ def generateBuildTasks(String flavor = "template") {
String libsDir = isTemplate ? "lib/libs/" : "lib/libs/tools/"
for (String target : supportedFlavorsBuildTypes[flavor]) {
File targetLibs = new File(libsDir + target)
+
+ String targetSuffix = target
+ if (target == "dev") {
+ targetSuffix = "debug.dev"
+ }
+
if (!excludeSconsBuildTasks || (targetLibs != null
&& targetLibs.isDirectory()
&& targetLibs.listFiles() != null
&& targetLibs.listFiles().length > 0)) {
+
String capitalizedTarget = target.capitalize()
if (isTemplate) {
- // Copy the generated aar library files to the build directory.
- tasks += "copy${capitalizedTarget}AARToAppModule"
- // Copy the generated aar library files to the bin directory.
- tasks += "copy${capitalizedTarget}AARToBin"
- // Copy the prebuilt binary templates to the bin directory.
- tasks += "copy${capitalizedTarget}BinaryToBin"
+ // Copy the Godot android library archive file into the app module libs directory.
+ // Depends on the library build task to ensure the AAR file is generated prior to copying.
+ String copyAARTaskName = "copy${capitalizedTarget}AARToAppModule"
+ if (tasks.findByName(copyAARTaskName) != null) {
+ buildTasks += tasks.getByName(copyAARTaskName)
+ } else {
+ buildTasks += tasks.create(name: copyAARTaskName, type: Copy) {
+ dependsOn ":lib:assembleTemplate${capitalizedTarget}"
+ from('lib/build/outputs/aar')
+ include("godot-lib.template_${targetSuffix}.aar")
+ into("app/libs/${target}")
+ }
+ }
+
+ // Copy the Godot android library archive file into the root bin directory.
+ // Depends on the library build task to ensure the AAR file is generated prior to copying.
+ String copyAARToBinTaskName = "copy${capitalizedTarget}AARToBin"
+ if (tasks.findByName(copyAARToBinTaskName) != null) {
+ buildTasks += tasks.getByName(copyAARToBinTaskName)
+ } else {
+ buildTasks += tasks.create(name: copyAARToBinTaskName, type: Copy) {
+ dependsOn ":lib:assembleTemplate${capitalizedTarget}"
+ from('lib/build/outputs/aar')
+ include("godot-lib.template_${targetSuffix}.aar")
+ into(binDir)
+ }
+ }
+
+ // Copy the generated binary template into the Godot bin directory.
+ // Depends on the app build task to ensure the binary is generated prior to copying.
+ String copyBinaryTaskName = "copy${capitalizedTarget}BinaryToBin"
+ if (tasks.findByName(copyBinaryTaskName) != null) {
+ buildTasks += tasks.getByName(copyBinaryTaskName)
+ } else {
+ buildTasks += tasks.create(name: copyBinaryTaskName, type: Copy) {
+ dependsOn ":app:assemble${capitalizedTarget}"
+ from("app/build/outputs/apk/${target}")
+ into(binDir)
+ include("android_${target}.apk")
+ }
+ }
} else {
// Copy the generated editor apk to the bin directory.
- tasks += "copyEditor${capitalizedTarget}ApkToBin"
+ String copyEditorApkTaskName = "copyEditor${capitalizedTarget}ApkToBin"
+ if (tasks.findByName(copyEditorApkTaskName) != null) {
+ buildTasks += tasks.getByName(copyEditorApkTaskName)
+ } else {
+ buildTasks += tasks.create(name: copyEditorApkTaskName, type: Copy) {
+ dependsOn ":editor:assemble${capitalizedTarget}"
+ from("editor/build/outputs/apk/${target}")
+ into(androidEditorBuildsDir)
+ include("android_editor-${target}*.apk")
+ }
+ }
+
// Copy the generated editor aab to the bin directory.
- tasks += "copyEditor${capitalizedTarget}AabToBin"
+ String copyEditorAabTaskName = "copyEditor${capitalizedTarget}AabToBin"
+ if (tasks.findByName(copyEditorAabTaskName) != null) {
+ buildTasks += tasks.getByName(copyEditorAabTaskName)
+ } else {
+ buildTasks += tasks.create(name: copyEditorAabTaskName, type: Copy) {
+ dependsOn ":editor:bundle${capitalizedTarget}"
+ from("editor/build/outputs/bundle/${target}")
+ into(androidEditorBuildsDir)
+ include("android_editor-${target}*.aab")
+ }
+ }
}
} else {
logger.lifecycle("No native shared libs for target $target. Skipping build.")
}
}
- return tasks
-}
-
-task copyEditorReleaseApkToBin(type: Copy) {
- dependsOn ':editor:assembleRelease'
- from('editor/build/outputs/apk/release')
- into(androidEditorBuildsDir)
- include('android_editor-release*.apk')
-}
-
-task copyEditorReleaseAabToBin(type: Copy) {
- dependsOn ':editor:bundleRelease'
- from('editor/build/outputs/bundle/release')
- into(androidEditorBuildsDir)
- include('android_editor-release*.aab')
-}
-
-task copyEditorDebugApkToBin(type: Copy) {
- dependsOn ':editor:assembleDebug'
- from('editor/build/outputs/apk/debug')
- into(androidEditorBuildsDir)
- include('android_editor-debug.apk')
-}
-
-task copyEditorDebugAabToBin(type: Copy) {
- dependsOn ':editor:bundleDebug'
- from('editor/build/outputs/bundle/debug')
- into(androidEditorBuildsDir)
- include('android_editor-debug.aab')
-}
-
-task copyEditorDevApkToBin(type: Copy) {
- dependsOn ':editor:assembleDev'
- from('editor/build/outputs/apk/dev')
- into(androidEditorBuildsDir)
- include('android_editor-dev.apk')
-}
-
-task copyEditorDevAabToBin(type: Copy) {
- dependsOn ':editor:bundleDev'
- from('editor/build/outputs/bundle/dev')
- into(androidEditorBuildsDir)
- include('android_editor-dev.aab')
+ return buildTasks
}
/**
@@ -301,7 +223,7 @@ task generateGodotTemplates {
*/
task generateDevTemplate {
// add parameter to set symbols to true
- gradle.startParameter.projectProperties += [doNotStrip: "true"]
+ project.ext.doNotStrip = "true"
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
dependsOn = generateBuildTasks("template")
diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle
index 55fe2a22fe..f9a3e10680 100644
--- a/platform/android/java/editor/build.gradle
+++ b/platform/android/java/editor/build.gradle
@@ -9,9 +9,10 @@ dependencies {
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
implementation project(":lib")
- implementation "androidx.window:window:1.2.0"
+ implementation "androidx.window:window:1.3.0"
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
+ implementation "org.bouncycastle:bcprov-jdk15to18:1.77"
}
ext {
diff --git a/platform/android/java/editor/src/main/assets/keystores/debug.keystore b/platform/android/java/editor/src/main/assets/keystores/debug.keystore
new file mode 100644
index 0000000000..3b7a97c8ee
--- /dev/null
+++ b/platform/android/java/editor/src/main/assets/keystores/debug.keystore
Binary files differ
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/ApkSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/ApkSigner.java
new file mode 100644
index 0000000000..49796a389e
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/ApkSigner.java
@@ -0,0 +1,1801 @@
+/*
+ * 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 static com.android.apksig.Constants.LIBRARY_PAGE_ALIGNMENT_BYTES;
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkSigningBlockNotFoundException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.ByteBufferDataSource;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.EocdRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+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.ReadableDataSink;
+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.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SignatureException;
+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 signer.
+ *
+ * <p>The 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.
+ *
+ * <p>Use {@link Builder} to obtain instances of this signer.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
+ */
+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_FILE_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<SignerConfig> mSignerConfigs;
+ private final SignerConfig mSourceStampSignerConfig;
+ private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
+ private final boolean mForceSourceStampOverwrite;
+ private final boolean mSourceStampTimestampEnabled;
+ private final Integer mMinSdkVersion;
+ private final int mRotationMinSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
+ private final boolean mV1SigningEnabled;
+ private final boolean mV2SigningEnabled;
+ private final boolean mV3SigningEnabled;
+ private final boolean mV4SigningEnabled;
+ private final boolean mAlignFileSize;
+ private final boolean mVerityEnabled;
+ private final boolean mV4ErrorReportingEnabled;
+ private final boolean mDebuggableApkPermitted;
+ private final boolean mOtherSignersSignaturesPreserved;
+ private final boolean mAlignmentPreserved;
+ private final int mLibraryPageAlignmentBytes;
+ 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 File mOutputV4File;
+
+ private final SigningCertificateLineage mSigningCertificateLineage;
+
+ private ApkSigner(
+ List<SignerConfig> signerConfigs,
+ SignerConfig sourceStampSignerConfig,
+ SigningCertificateLineage sourceStampSigningCertificateLineage,
+ boolean forceSourceStampOverwrite,
+ boolean sourceStampTimestampEnabled,
+ Integer minSdkVersion,
+ int rotationMinSdkVersion,
+ boolean rotationTargetsDevRelease,
+ boolean v1SigningEnabled,
+ boolean v2SigningEnabled,
+ boolean v3SigningEnabled,
+ boolean v4SigningEnabled,
+ boolean alignFileSize,
+ boolean verityEnabled,
+ boolean v4ErrorReportingEnabled,
+ boolean debuggableApkPermitted,
+ boolean otherSignersSignaturesPreserved,
+ boolean alignmentPreserved,
+ int libraryPageAlignmentBytes,
+ String createdBy,
+ ApkSignerEngine signerEngine,
+ File inputApkFile,
+ DataSource inputApkDataSource,
+ File outputApkFile,
+ DataSink outputApkDataSink,
+ DataSource outputApkDataSource,
+ File outputV4File,
+ SigningCertificateLineage signingCertificateLineage) {
+
+ mSignerConfigs = signerConfigs;
+ mSourceStampSignerConfig = sourceStampSignerConfig;
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+ mForceSourceStampOverwrite = forceSourceStampOverwrite;
+ mSourceStampTimestampEnabled = sourceStampTimestampEnabled;
+ mMinSdkVersion = minSdkVersion;
+ mRotationMinSdkVersion = rotationMinSdkVersion;
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
+ mV1SigningEnabled = v1SigningEnabled;
+ mV2SigningEnabled = v2SigningEnabled;
+ mV3SigningEnabled = v3SigningEnabled;
+ mV4SigningEnabled = v4SigningEnabled;
+ mAlignFileSize = alignFileSize;
+ mVerityEnabled = verityEnabled;
+ mV4ErrorReportingEnabled = v4ErrorReportingEnabled;
+ mDebuggableApkPermitted = debuggableApkPermitted;
+ mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
+ mAlignmentPreserved = alignmentPreserved;
+ mLibraryPageAlignmentBytes = libraryPageAlignmentBytes;
+ mCreatedBy = createdBy;
+
+ mSignerEngine = signerEngine;
+
+ mInputApkFile = inputApkFile;
+ mInputApkDataSource = inputApkDataSource;
+
+ mOutputApkFile = outputApkFile;
+ mOutputApkDataSink = outputApkDataSink;
+ mOutputApkDataSource = outputApkDataSource;
+
+ mOutputV4File = outputV4File;
+
+ 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<CentralDirectoryRecord> inputCdRecords =
+ parseZipCentralDirectory(inputCd, inputZipSections);
+
+ List<Hints.PatternWithRange> pinPatterns =
+ extractPinPatterns(inputCdRecords, inputApkLfhSection);
+ List<Hints.ByteRange> 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<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs =
+ new ArrayList<>(mSignerConfigs.size());
+ for (SignerConfig signerConfig : mSignerConfigs) {
+ DefaultApkSignerEngine.SignerConfig.Builder signerConfigBuilder =
+ new DefaultApkSignerEngine.SignerConfig.Builder(
+ signerConfig.getName(),
+ signerConfig.getPrivateKey(),
+ signerConfig.getCertificates(),
+ signerConfig.getDeterministicDsaSigning());
+ int signerMinSdkVersion = signerConfig.getMinSdkVersion();
+ SigningCertificateLineage signerLineage =
+ signerConfig.getSigningCertificateLineage();
+ if (signerMinSdkVersion > 0) {
+ signerConfigBuilder.setLineageForMinSdkVersion(signerLineage,
+ signerMinSdkVersion);
+ }
+ engineSignerConfigs.add(signerConfigBuilder.build());
+ }
+ DefaultApkSignerEngine.Builder signerEngineBuilder =
+ new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion)
+ .setV1SigningEnabled(mV1SigningEnabled)
+ .setV2SigningEnabled(mV2SigningEnabled)
+ .setV3SigningEnabled(mV3SigningEnabled)
+ .setVerityEnabled(mVerityEnabled)
+ .setDebuggableApkPermitted(mDebuggableApkPermitted)
+ .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved)
+ .setSigningCertificateLineage(mSigningCertificateLineage)
+ .setMinSdkVersionForRotation(mRotationMinSdkVersion)
+ .setRotationTargetsDevRelease(mRotationTargetsDevRelease);
+ if (mCreatedBy != null) {
+ signerEngineBuilder.setCreatedBy(mCreatedBy);
+ }
+ if (mSourceStampSignerConfig != null) {
+ signerEngineBuilder.setStampSignerConfig(
+ new DefaultApkSignerEngine.SignerConfig.Builder(
+ mSourceStampSignerConfig.getName(),
+ mSourceStampSignerConfig.getPrivateKey(),
+ mSourceStampSignerConfig.getCertificates(),
+ mSourceStampSignerConfig.getDeterministicDsaSigning())
+ .build());
+ signerEngineBuilder.setSourceStampTimestampEnabled(mSourceStampTimestampEnabled);
+ }
+ if (mSourceStampSigningCertificateLineage != null) {
+ signerEngineBuilder.setSourceStampSigningCertificateLineage(
+ mSourceStampSigningCertificateLineage);
+ }
+ 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<CentralDirectoryRecord> 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;
+ byte[] sourceStampCertificateDigest = null;
+ Map<String, CentralDirectoryRecord> 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.
+ }
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(entryName)) {
+ try {
+ sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ inputApkLfhSection, inputCdRecord, inputApkLfhSection.size());
+ } catch (ZipFormatException ex) {
+ throw new ApkFormatException("Bad source stamp entry");
+ }
+ continue; // Existing source stamp is handled below as 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;
+ OutputSizeAndDataOffset outputLfrResult =
+ outputInputJarEntryLfhRecord(
+ inputApkLfhSection,
+ inputLocalFileRecord,
+ outputApkOut,
+ outputLocalFileHeaderOffset);
+ outputOffset += outputLfrResult.outputBytes;
+ long outputDataOffset =
+ outputLocalFileHeaderOffset + outputLfrResult.dataOffsetBytes;
+
+ if (pinPatterns != null) {
+ boolean pinFileHeader = false;
+ for (Hints.PatternWithRange pinPattern : pinPatterns) {
+ if (pinPattern.matcher(inputCdRecord.getName()).matches()) {
+ Hints.ByteRange dataRange =
+ new Hints.ByteRange(outputDataOffset, outputOffset);
+ Hints.ByteRange pinRange =
+ pinPattern.ClampToAbsoluteByteRange(dataRange);
+ if (pinRange != null) {
+ pinFileHeader = true;
+ pinByteRanges.add(pinRange);
+ }
+ }
+ }
+ if (pinFileHeader) {
+ pinByteRanges.add(
+ new Hints.ByteRange(outputLocalFileHeaderOffset, outputDataOffset));
+ }
+ }
+
+ // 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<CentralDirectoryRecord> 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);
+ }
+ }
+
+ if (lastModifiedDateForNewEntries == -1) {
+ lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS)
+ lastModifiedTimeForNewEntries = 0;
+ }
+
+ // Step 7. Generate and output SourceStamp certificate hash, if necessary. This may output
+ // more Local File Header + data entries and add to the list of output Central Directory
+ // records.
+ if (signerEngine.isEligibleForSourceStamp()) {
+ byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest();
+ if (mForceSourceStampOverwrite
+ || sourceStampCertificateDigest == null
+ || Arrays.equals(uncompressedData, sourceStampCertificateDigest)) {
+ outputOffset +=
+ outputDataToOutputApk(
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME,
+ uncompressedData,
+ outputOffset,
+ outputCdRecords,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ outputApkOut);
+ } else {
+ throw new ApkFormatException(
+ String.format(
+ "Cannot generate SourceStamp. APK contains an existing entry with"
+ + " the name: %s, and it is different than the provided source"
+ + " stamp certificate",
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME));
+ }
+ }
+
+ // Step 7.5. Generate pinlist.meta file if necessary.
+ // This has to be before the step 8 so that the file is signed.
+ if (pinByteRanges != null) {
+ // Covers JAR signature and zip central dir entry.
+ // The signature files don't have to be pinned, but pinning them isn't that wasteful
+ // since the total size is small.
+ pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE));
+ String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME;
+ byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges);
+
+ requestOutputEntryInspection(signerEngine, entryName, uncompressedData);
+ outputOffset +=
+ outputDataToOutputApk(
+ entryName,
+ uncompressedData,
+ outputOffset,
+ outputCdRecords,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ outputApkOut);
+ }
+
+ // Step 8. 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) {
+ for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry :
+ outputJarSignatureRequest.getAdditionalJarEntries()) {
+ String entryName = entry.getName();
+ byte[] uncompressedData = entry.getData();
+
+ requestOutputEntryInspection(signerEngine, entryName, uncompressedData);
+ outputOffset +=
+ outputDataToOutputApk(
+ entryName,
+ uncompressedData,
+ outputOffset,
+ outputCdRecords,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ outputApkOut);
+ }
+ outputJarSignatureRequest.done();
+ }
+
+ // Step 9. 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 10. Construct output ZIP End of Central Directory record in an in-memory buffer
+ // because it can be adjusted in Step 11 due to signing block.
+ // - CD offset (it's shifted by signing block)
+ // - Comments (when the output file needs to be sized 4k-aligned)
+ ByteBuffer outputEocd =
+ EocdRecord.createWithModifiedCentralDirectoryInfo(
+ inputZipSections.getZipEndOfCentralDirectory(),
+ outputCentralDirRecordCount,
+ outputCentralDirDataSource.size(),
+ outputCentralDirStartOffset);
+
+ // Step 11. Generate and output APK Signature Scheme v2 and/or v3 signatures and/or
+ // SourceStamp 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();
+ byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
+ outputApkSigningBlockRequest.done();
+
+ long fileSize =
+ outputCentralDirStartOffset
+ + outputCentralDirDataSource.size()
+ + padding
+ + outputApkSigningBlock.length
+ + outputEocd.remaining();
+ if (mAlignFileSize && (fileSize % ANDROID_FILE_ALIGNMENT_BYTES != 0)) {
+ int eocdPadding =
+ (int)
+ (ANDROID_FILE_ALIGNMENT_BYTES
+ - fileSize % ANDROID_FILE_ALIGNMENT_BYTES);
+ // Replace EOCD with padding one so that output file size can be the multiples of
+ // alignment.
+ outputEocd = EocdRecord.createWithPaddedComment(outputEocd, eocdPadding);
+
+ // Since EoCD has changed, we need to regenerate signing block as well.
+ outputApkSigningBlockRequest =
+ signerEngine.outputZipSections2(
+ outputApkIn,
+ new ByteBufferDataSource(outputCentralDir),
+ DataSources.asDataSource(outputEocd));
+ outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock();
+ outputApkSigningBlockRequest.done();
+ }
+
+ outputApkOut.consume(ByteBuffer.allocate(padding));
+ outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length);
+ ZipUtils.setZipEocdCentralDirectoryOffset(
+ outputEocd,
+ outputCentralDirStartOffset + padding + outputApkSigningBlock.length);
+ }
+
+ // Step 12. Output ZIP Central Directory and ZIP End of Central Directory
+ outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut);
+ outputApkOut.consume(outputEocd);
+ signerEngine.outputDone();
+
+ // Step 13. Generate and output APK Signature Scheme v4 signatures, if necessary.
+ if (mV4SigningEnabled) {
+ signerEngine.signV4(outputApkIn, mOutputV4File, !mV4ErrorReportingEnabled);
+ }
+ }
+
+ private static void requestOutputEntryInspection(
+ ApkSignerEngine signerEngine,
+ String entryName,
+ byte[] uncompressedData)
+ throws IOException {
+ ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
+ signerEngine.outputJarEntry(entryName);
+ if (inspectEntryRequest != null) {
+ inspectEntryRequest.getDataSink().consume(
+ uncompressedData, 0, uncompressedData.length);
+ inspectEntryRequest.done();
+ }
+ }
+
+ private static long outputDataToOutputApk(
+ String entryName,
+ byte[] uncompressedData,
+ long localFileHeaderOffset,
+ List<CentralDirectoryRecord> outputCdRecords,
+ int lastModifiedTimeForNewEntries,
+ int lastModifiedDateForNewEntries,
+ DataSink outputApkOut)
+ throws IOException {
+ ZipUtils.DeflateResult deflateResult = ZipUtils.deflate(ByteBuffer.wrap(uncompressedData));
+ byte[] compressedData = deflateResult.output;
+ long uncompressedDataCrc32 = deflateResult.inputCrc32;
+ long numOfDataBytes =
+ LocalFileRecord.outputRecordWithDeflateCompressedData(
+ entryName,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ compressedData,
+ uncompressedDataCrc32,
+ uncompressedData.length,
+ outputApkOut);
+ outputCdRecords.add(
+ CentralDirectoryRecord.createWithDeflateCompressedData(
+ entryName,
+ lastModifiedTimeForNewEntries,
+ lastModifiedDateForNewEntries,
+ uncompressedDataCrc32,
+ compressedData.length,
+ uncompressedData.length,
+ localFileHeaderOffset));
+ return numOfDataBytes;
+ }
+
+ 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 class OutputSizeAndDataOffset {
+ public long outputBytes;
+ public long dataOffsetBytes;
+
+ public OutputSizeAndDataOffset(long outputBytes, long dataOffsetBytes) {
+ this.outputBytes = outputBytes;
+ this.dataOffsetBytes = dataOffsetBytes;
+ }
+ }
+
+ private OutputSizeAndDataOffset outputInputJarEntryLfhRecord(
+ DataSource inputLfhSection,
+ LocalFileRecord inputRecord,
+ DataSink outputLfhSection,
+ long outputOffset)
+ throws IOException {
+ long inputOffset = inputRecord.getStartOffsetInArchive();
+ if (inputOffset == outputOffset && mAlignmentPreserved) {
+ // This record's data will be aligned same as in the input APK.
+ return new OutputSizeAndDataOffset(
+ inputRecord.outputRecord(inputLfhSection, outputLfhSection),
+ inputRecord.getDataStartOffsetInRecord());
+ }
+ int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord);
+ if ((dataAlignmentMultiple <= 1)
+ || ((inputOffset % dataAlignmentMultiple) == (outputOffset % dataAlignmentMultiple)
+ && mAlignmentPreserved)) {
+ // This record's data will be aligned same as in the input APK.
+ return new OutputSizeAndDataOffset(
+ inputRecord.outputRecord(inputLfhSection, outputLfhSection),
+ inputRecord.getDataStartOffsetInRecord());
+ }
+
+ long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord();
+ if ((inputDataStartOffset % dataAlignmentMultiple) != 0 && mAlignmentPreserved) {
+ // This record's data is not aligned in the input APK. No need to align it in the
+ // output.
+ return new OutputSizeAndDataOffset(
+ inputRecord.outputRecord(inputLfhSection, outputLfhSection),
+ inputRecord.getDataStartOffsetInRecord());
+ }
+
+ // 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);
+ long dataOffset =
+ (long) inputRecord.getDataStartOffsetInRecord()
+ + aligningExtra.remaining()
+ - inputRecord.getExtra().remaining();
+ return new OutputSizeAndDataOffset(
+ inputRecord.outputRecordWithModifiedExtra(
+ inputLfhSection, aligningExtra, outputLfhSection),
+ dataOffset);
+ }
+
+ private 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")) ? mLibraryPageAlignmentBytes : 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<CentralDirectoryRecord> parseZipCentralDirectory(
+ ByteBuffer cd, ApkUtils.ZipSections apkSections) throws ApkFormatException {
+ long cdOffset = apkSections.getZipCentralDirectoryOffset();
+ int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
+ List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
+ Set<String> 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<CentralDirectoryRecord> 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<CentralDirectoryRecord> 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<Hints.PatternWithRange> extractPinPatterns(
+ List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord pinListCdRecord =
+ findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME);
+ List<Hints.PatternWithRange> 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<CentralDirectoryRecord> 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.
+ *
+ * <p>Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerConfig {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List<X509Certificate> mCertificates;
+ private final boolean mDeterministicDsaSigning;
+ private final int mMinSdkVersion;
+ private final SigningCertificateLineage mSigningCertificateLineage;
+
+ private SignerConfig(Builder builder) {
+ mName = builder.mName;
+ mPrivateKey = builder.mPrivateKey;
+ mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates));
+ mDeterministicDsaSigning = builder.mDeterministicDsaSigning;
+ mMinSdkVersion = builder.mMinSdkVersion;
+ mSigningCertificateLineage = builder.mSigningCertificateLineage;
+ }
+
+ /** 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<X509Certificate> getCertificates() {
+ return mCertificates;
+ }
+
+ /**
+ * If this signer is a DSA signer, whether or not the signing is done deterministically.
+ */
+ public boolean getDeterministicDsaSigning() {
+ return mDeterministicDsaSigning;
+ }
+
+ /** Returns the minimum SDK version for which this signer should be used. */
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ /** Returns the {@link SigningCertificateLineage} for this signer. */
+ public SigningCertificateLineage getSigningCertificateLineage() {
+ return mSigningCertificateLineage;
+ }
+
+ /** Builder of {@link SignerConfig} instances. */
+ public static class Builder {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List<X509Certificate> mCertificates;
+ private final boolean mDeterministicDsaSigning;
+
+ private int mMinSdkVersion;
+ private SigningCertificateLineage mSigningCertificateLineage;
+
+ /**
+ * 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<X509Certificate> certificates) {
+ this(name, privateKey, certificates, false);
+ }
+
+ /**
+ * 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}.
+ * @param deterministicDsaSigning When signing using DSA, whether or not the
+ * deterministic variant (RFC6979) should be used.
+ */
+ public Builder(
+ String name,
+ PrivateKey privateKey,
+ List<X509Certificate> certificates,
+ boolean deterministicDsaSigning) {
+ if (name.isEmpty()) {
+ throw new IllegalArgumentException("Empty name");
+ }
+ mName = name;
+ mPrivateKey = privateKey;
+ mCertificates = new ArrayList<>(certificates);
+ mDeterministicDsaSigning = deterministicDsaSigning;
+ }
+
+ /** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */
+ public Builder setMinSdkVersion(int minSdkVersion) {
+ return setLineageForMinSdkVersion(null, minSdkVersion);
+ }
+
+ /**
+ * Sets the specified {@code minSdkVersion} as the minimum Android platform version
+ * (API level) for which the provided {@code lineage} (where applicable) should be used
+ * to produce the APK's signature. This method is useful if callers want to specify a
+ * particular rotated signer or lineage with restricted capabilities for later
+ * platform releases.
+ *
+ * <p><em>Note:</em>>The V1 and V2 signature schemes do not support key rotation and
+ * signing lineages with capabilities; only an app's original signer(s) can be used for
+ * the V1 and V2 signature blocks. Because of this, only a value of {@code
+ * minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was
+ * introduced can be specified.
+ *
+ * <p><em>Note:</em>Due to limitations with platform targeting in the V3.0 signature
+ * scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in
+ * the current {@code SignerConfig} being used in the V3.0 signing block and applied to
+ * Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for
+ * subsequent {@code SignerConfig} instances). Because of this, only a single {@code
+ * SignerConfig} can be instantiated with a minimum SDK version <= 32.
+ *
+ * @param lineage the {@code SigningCertificateLineage} to target the specified {@code
+ * minSdkVersion}
+ * @param minSdkVersion the minimum SDK version for which this {@code SignerConfig}
+ * should be used
+ * @return this {@code Builder} instance
+ *
+ * @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the
+ * certificate provided in the constructor is not in the specified {@code lineage}.
+ */
+ public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage,
+ int minSdkVersion) {
+ if (minSdkVersion < AndroidSdkVersion.P) {
+ throw new IllegalArgumentException(
+ "SDK targeted signing config is only supported with the V3 signature "
+ + "scheme on Android P (SDK version "
+ + AndroidSdkVersion.P + ") and later");
+ }
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ minSdkVersion = AndroidSdkVersion.P;
+ }
+ mMinSdkVersion = minSdkVersion;
+ // If a lineage is provided, ensure the signing certificate for this signer is in
+ // the lineage; in the case of multiple signing certificates, the first is always
+ // used in the lineage.
+ if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) {
+ throw new IllegalArgumentException(
+ "The provided lineage does not contain the signing certificate, "
+ + mCertificates.get(0).getSubjectDN()
+ + ", for this SignerConfig");
+ }
+ mSigningCertificateLineage = lineage;
+ return this;
+ }
+
+ /**
+ * Returns a new {@code SignerConfig} instance configured based on the configuration of
+ * this builder.
+ */
+ public SignerConfig build() {
+ return new SignerConfig(this);
+ }
+ }
+ }
+
+ /**
+ * Builder of {@link ApkSigner} instances.
+ *
+ * <p>The builder requires the following information to construct a working {@code ApkSigner}:
+ *
+ * <ul>
+ * <li>Signer configs or {@link ApkSignerEngine} -- provided in the constructor,
+ * <li>APK to be signed -- see {@link #setInputApk(File) setInputApk} variants,
+ * <li>where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk}
+ * variants.
+ * </ul>
+ */
+ public static class Builder {
+ private final List<SignerConfig> mSignerConfigs;
+ private SignerConfig mSourceStampSignerConfig;
+ private SigningCertificateLineage mSourceStampSigningCertificateLineage;
+ private boolean mForceSourceStampOverwrite = false;
+ private boolean mSourceStampTimestampEnabled = true;
+ private boolean mV1SigningEnabled = true;
+ private boolean mV2SigningEnabled = true;
+ private boolean mV3SigningEnabled = true;
+ private boolean mV4SigningEnabled = true;
+ private boolean mAlignFileSize = false;
+ private boolean mVerityEnabled = false;
+ private boolean mV4ErrorReportingEnabled = false;
+ private boolean mDebuggableApkPermitted = true;
+ private boolean mOtherSignersSignaturesPreserved;
+ private boolean mAlignmentPreserved = false;
+ private int mLibraryPageAlignmentBytes = LIBRARY_PAGE_ALIGNMENT_BYTES;
+ private String mCreatedBy;
+ private Integer mMinSdkVersion;
+ private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+ private boolean mRotationTargetsDevRelease = false;
+
+ private final ApkSignerEngine mSignerEngine;
+
+ private File mInputApkFile;
+ private DataSource mInputApkDataSource;
+
+ private File mOutputApkFile;
+ private DataSink mOutputApkDataSink;
+ private DataSource mOutputApkDataSource;
+
+ private File mOutputV4File;
+
+ 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)}.
+ *
+ * <p>{@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where more
+ * control over low-level details of signing is desired.
+ */
+ public Builder(List<SignerConfig> 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 signing configuration of the source stamp to be embedded in the APK. */
+ public Builder setSourceStampSignerConfig(SignerConfig sourceStampSignerConfig) {
+ mSourceStampSignerConfig = sourceStampSignerConfig;
+ return this;
+ }
+
+ /**
+ * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of
+ * signing certificate rotation for certificates previously used to sign source stamps.
+ */
+ public Builder setSourceStampSigningCertificateLineage(
+ SigningCertificateLineage sourceStampSigningCertificateLineage) {
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should overwrite existing source stamp, if found.
+ *
+ * @param force {@code true} to require the APK to be overwrite existing source stamp
+ */
+ public Builder setForceSourceStampOverwrite(boolean force) {
+ mForceSourceStampOverwrite = force;
+ return this;
+ }
+
+ /**
+ * Sets whether the source stamp should contain the timestamp attribute with the time
+ * at which the source stamp was signed.
+ */
+ public Builder setSourceStampTimestampEnabled(boolean value) {
+ mSourceStampTimestampEnabled = value;
+ return this;
+ }
+
+ /**
+ * 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.
+ *
+ * <p>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.
+ *
+ * <p>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 location of the V4 output file. {@code ApkSigner} will create this file if it
+ * doesn't exist.
+ */
+ public Builder setV4SignatureOutputFile(File v4SignatureOutputFile) {
+ if (v4SignatureOutputFile == null) {
+ throw new NullPointerException("v4HashRootOutputFile == null");
+ }
+ mOutputV4File = v4SignatureOutputFile;
+ 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}.
+ *
+ * <p><em>Note:</em> This method may result in APK signatures which don't verify on some
+ * Android platform versions supported by the APK.
+ *
+ * <p><em>Note:</em> 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 the minimum Android platform version (API Level) for which an APK's rotated signing
+ * key should be used to produce the APK's signature. The original signing key for the APK
+ * will be used for all previous platform versions. If a rotated key with signing lineage is
+ * not provided then this method is a noop. This method is useful for overriding the
+ * default behavior where Android T is set as the minimum API level for rotation.
+ *
+ * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+ * in the original V3 signing block being used without platform targeting.
+ *
+ * <p><em>Note:</em> 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 setMinSdkVersionForRotation(int minSdkVersion) {
+ checkInitializedWithoutEngine();
+ // If the provided SDK version does not support v3.1, then use the default SDK version
+ // with rotation support.
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT;
+ } else {
+ mRotationMinSdkVersion = minSdkVersion;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the rotation-min-sdk-version is intended to target a development release;
+ * this is primarily required after the T SDK is finalized, and an APK needs to target U
+ * during its development cycle for rotation.
+ *
+ * <p>This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ *
+ * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
+ *
+ * <p>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.
+ *
+ * <p><em>Note:</em> 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 <a
+ * href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">JAR
+ * signing</a>
+ */
+ 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).
+ *
+ * <p>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)}.
+ *
+ * <p><em>Note:</em> 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 <a href="https://source.android.com/security/apksigning/v2.html">APK Signature
+ * Scheme v2</a>
+ */
+ 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).
+ *
+ * <p>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)}.
+ *
+ * <p><em>Note:</em> This method may only be invoked when this builder is not initialized
+ * with an {@link ApkSignerEngine}.
+ *
+ * <p><em>Note:</em> 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 using APK Signature Scheme v4.
+ *
+ * <p>V4 signing requires that the APK be v2 or v3 signed.
+ *
+ * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme v2
+ * or v3 and generate an v4 signature file
+ */
+ public Builder setV4SigningEnabled(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mV4SigningEnabled = enabled;
+ mV4ErrorReportingEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether errors during v4 signing should be reported and halt the signing process.
+ *
+ * <p>Error reporting for v4 signing is disabled by default, but will be enabled if the
+ * caller invokes {@link #setV4SigningEnabled} with a value of true. This method is useful
+ * for tools that enable v4 signing by default but don't want to fail the signing process if
+ * the user did not explicitly request the v4 signing.
+ *
+ * @param enabled {@code false} to prevent errors encountered during the V4 signing from
+ * halting the signing process
+ */
+ public Builder setV4ErrorReportingEnabled(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mV4ErrorReportingEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets whether the output APK files should be sized as multiples of 4K.
+ *
+ * <p><em>Note:</em> 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 setAlignFileSize(boolean alignFileSize) {
+ checkInitializedWithoutEngine();
+ mAlignFileSize = alignFileSize;
+ return this;
+ }
+
+ /**
+ * Sets whether to enable the verity signature algorithm for the v2 and v3 signature
+ * schemes.
+ *
+ * @param enabled {@code true} to enable the verity signature algorithm for inclusion in the
+ * v2 and v3 signature blocks.
+ */
+ public Builder setVerityEnabled(boolean enabled) {
+ checkInitializedWithoutEngine();
+ mVerityEnabled = enabled;
+ 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}.
+ *
+ * <p>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.
+ *
+ * <p><em>Note:</em> 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.
+ *
+ * <p>By default, signatures of other signers are omitted from the output APK.
+ *
+ * <p><em>Note:</em> 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.
+ *
+ * <p><em>Note:</em> 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;
+ }
+
+ /**
+ * Sets whether the existing alignment within the APK should be preserved; the
+ * default for this setting is false. When this value is false, the value provided to
+ * {@link #setLibraryPageAlignmentBytes(int)} will be used to page align native library
+ * files and 4 bytes will be used to align all other uncompressed files.
+ */
+ public Builder setAlignmentPreserved(boolean alignmentPreserved) {
+ mAlignmentPreserved = alignmentPreserved;
+ return this;
+ }
+
+ /**
+ * Sets the number of bytes to be used to page align native library files in the APK; the
+ * default for this setting is {@link Constants#LIBRARY_PAGE_ALIGNMENT_BYTES}.
+ */
+ public Builder setLibraryPageAlignmentBytes(int libraryPageAlignmentBytes) {
+ mLibraryPageAlignmentBytes = libraryPageAlignmentBytes;
+ 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;
+ }
+
+ // If V4 signing is not explicitly set, and V2/V3 signing is disabled, then V4 signing
+ // must be disabled as well as it is dependent on V2/V3.
+ if (mV4SigningEnabled && !mV2SigningEnabled && !mV3SigningEnabled) {
+ if (!mV4ErrorReportingEnabled) {
+ mV4SigningEnabled = false;
+ } else {
+ throw new IllegalStateException(
+ "APK Signature Scheme v4 signing requires at least "
+ + "v2 or v3 signing to be enabled");
+ }
+ }
+
+ // TODO - if v3 signing is enabled, check provided signers and history to see if valid
+
+ return new ApkSigner(
+ mSignerConfigs,
+ mSourceStampSignerConfig,
+ mSourceStampSigningCertificateLineage,
+ mForceSourceStampOverwrite,
+ mSourceStampTimestampEnabled,
+ mMinSdkVersion,
+ mRotationMinSdkVersion,
+ mRotationTargetsDevRelease,
+ mV1SigningEnabled,
+ mV2SigningEnabled,
+ mV3SigningEnabled,
+ mV4SigningEnabled,
+ mAlignFileSize,
+ mVerityEnabled,
+ mV4ErrorReportingEnabled,
+ mDebuggableApkPermitted,
+ mOtherSignersSignaturesPreserved,
+ mAlignmentPreserved,
+ mLibraryPageAlignmentBytes,
+ mCreatedBy,
+ mSignerEngine,
+ mInputApkFile,
+ mInputApkDataSource,
+ mOutputApkFile,
+ mOutputApkDataSink,
+ mOutputApkDataSource,
+ mOutputV4File,
+ mSigningCertificateLineage);
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/ApkSignerEngine.java b/platform/android/java/editor/src/main/java/com/android/apksig/ApkSignerEngine.java
new file mode 100644
index 0000000000..c79f232707
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/ApkSignerEngine.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * APK signing logic which is independent of how input and output APKs are stored, parsed, and
+ * generated.
+ *
+ * <p><h3>Operating Model</h3>
+ *
+ * The abstract operating model is that there is an input APK which is being signed, thus producing
+ * an output APK. In reality, there may be just an output APK being built from scratch, or the input
+ * APK and the output APK may be the same file. Because this engine does not deal with reading and
+ * writing files, it can handle all of these scenarios.
+ *
+ * <p>The engine is stateful and thus cannot be used for signing multiple APKs. However, once
+ * the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
+ * This may be more efficient than signing the APK using a new instance of the engine. See
+ * <a href="#incremental">Incremental Operation</a>.
+ *
+ * <p>In the engine's operating model, a signed APK is produced as follows.
+ * <ol>
+ * <li>JAR entries to be signed are output,</li>
+ * <li>JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
+ * output,</li>
+ * <li>JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
+ * to the output.</li>
+ * </ol>
+ *
+ * <p>The input APK may contain JAR entries which, depending on the engine's configuration, may or
+ * may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
+ * engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
+ * which tells the client whether the input JAR entry needs to be output. This avoids the need for
+ * the client to hard-code the aspects of APK signing which determine which parts of input must be
+ * ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
+ * client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
+ * APK.
+ *
+ * <p>To use the engine to sign an input APK (or a collection of JAR entries), follow these
+ * steps:
+ * <ol>
+ * <li>Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
+ * for signing multiple APKs.</li>
+ * <li>Locate the input APK's APK Signing Block and provide it to
+ * {@link #inputApkSigningBlock(DataSource)}.</li>
+ * <li>For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
+ * whether this entry should be output. The engine may request to inspect the entry.</li>
+ * <li>For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
+ * inspect the entry.</li>
+ * <li>Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
+ * that additional JAR entries are output. These entries comprise the output APK's JAR
+ * signature.</li>
+ * <li>Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
+ * invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that
+ * an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
+ * output APK's APK Signature Scheme v2 signature.</li>
+ * <li>Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
+ * confirm that the output APK is signed.</li>
+ * <li>Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
+ * engine free any resources it no longer needs.
+ * </ol>
+ *
+ * <p>Some invocations of the engine may provide the client with a task to perform. The client is
+ * expected to perform all requested tasks before proceeding to the next stage of signing. See
+ * documentation of each method about the deadlines for performing the tasks requested by the
+ * method.
+ *
+ * <p><h3 id="incremental">Incremental Operation</h3></a>
+ *
+ * The engine supports incremental operation where a signed APK is produced, then modified and
+ * re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
+ * by the developer. Re-signing may be more efficient than signing from scratch.
+ *
+ * <p>To use the engine in incremental mode, keep notifying the engine of changes to the APK through
+ * {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
+ * {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
+ * and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
+ * these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
+ * APK.
+ *
+ * <p><h3>Output-only Operation</h3>
+ *
+ * The engine's abstract operating model consists of an input APK and an output APK. However, it is
+ * possible to use the engine in output-only mode where the engine's {@code input...} methods are
+ * not invoked. In this mode, the engine has less control over output because it cannot request that
+ * some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
+ * signed and will report an error if cannot do so.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
+ */
+public interface ApkSignerEngine extends Closeable {
+
+ default void setExecutor(RunnablesExecutor executor) {
+ throw new UnsupportedOperationException("setExecutor method is not implemented");
+ }
+
+ /**
+ * Initializes the signer engine with the data already present in the apk (if any). There
+ * might already be data that can be reused if the entries has not been changed.
+ *
+ * @param manifestBytes
+ * @param entryNames
+ * @return set of entry names which were processed by the engine during the initialization, a
+ * subset of entryNames
+ */
+ default Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
+ throw new UnsupportedOperationException("initWith method is not implemented");
+ }
+
+ /**
+ * Indicates to this engine that the input APK contains the provided APK Signing Block. The
+ * block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
+ *
+ * @param apkSigningBlock APK signing block of the input APK. The provided data source is
+ * guaranteed to not be used by the engine after this method terminates.
+ *
+ * @throws IOException if an I/O error occurs while reading the APK Signing Block
+ * @throws ApkFormatException if the APK Signing Block is malformed
+ * @throws IllegalStateException if this engine is closed
+ */
+ void inputApkSigningBlock(DataSource apkSigningBlock)
+ throws IOException, ApkFormatException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was encountered in the input APK.
+ *
+ * <p>When an input entry is updated/changed, it's OK to not invoke
+ * {@link #inputJarEntryRemoved(String)} before invoking this method.
+ *
+ * @return instructions about how to proceed with this entry
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was output.
+ *
+ * <p>It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
+ * requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
+ * data requested by the engine.
+ *
+ * <p>When an already output entry is updated/changed, it's OK to not invoke
+ * {@link #outputJarEntryRemoved(String)} before invoking this method.
+ *
+ * @return request to inspect the entry or {@code null} if the engine does not need to inspect
+ * the entry. The request must be fulfilled before {@link #outputJarEntries()} is
+ * invoked.
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was removed from the input. It's safe
+ * to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
+ *
+ * @return output policy of this JAR entry. The policy indicates how this input entry affects
+ * the output APK. The client of this engine should use this information to determine
+ * how the removal of this input APK's JAR entry affects the output APK.
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
+ throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that the specified JAR entry was removed from the output. It's safe
+ * to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
+ *
+ * @throws IllegalStateException if this engine is closed
+ */
+ void outputJarEntryRemoved(String entryName) throws IllegalStateException;
+
+ /**
+ * Indicates to this engine that all JAR entries have been output.
+ *
+ * @return request to add JAR signature to the output or {@code null} if there is no need to add
+ * a JAR signature. The request will contain additional JAR entries to be output. The
+ * request must be fulfilled before
+ * {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked.
+ *
+ * @throws ApkFormatException if the APK is malformed in a way which is preventing this engine
+ * from producing a valid signature. For example, if the engine uses the provided
+ * {@code META-INF/MANIFEST.MF} as a template and the file is malformed.
+ * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+ * cryptographic algorithm implementation is missing
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws SignatureException if an error occurred while generating a signature
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries, or if the engine is closed
+ */
+ OutputJarSignatureRequest outputJarEntries()
+ throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
+ SignatureException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the ZIP sections comprising the output APK have been output.
+ *
+ * <p>The provided data sources are guaranteed to not be used by the engine after this method
+ * terminates.
+ *
+ * @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource,
+ * DataSource)}.
+ *
+ * @param zipEntries the section of ZIP archive containing Local File Header records and data of
+ * the ZIP entries. In a well-formed archive, this section starts at the start of the
+ * archive and extends all the way to the ZIP Central Directory.
+ * @param zipCentralDirectory ZIP Central Directory section
+ * @param zipEocd ZIP End of Central Directory (EoCD) record
+ *
+ * @return request to add an APK Signing Block to the output or {@code null} if the output must
+ * not contain an APK Signing Block. The request must be fulfilled before
+ * {@link #outputDone()} is invoked.
+ *
+ * @throws IOException if an I/O error occurs while reading the provided ZIP sections
+ * @throws ApkFormatException if the provided APK is malformed in a way which prevents this
+ * engine from producing a valid signature. For example, if the APK Signing Block
+ * provided to the engine is malformed.
+ * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+ * cryptographic algorithm implementation is missing
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws SignatureException if an error occurred while generating a signature
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries or to output JAR signature, or if the engine is closed
+ */
+ @Deprecated
+ OutputApkSigningBlockRequest outputZipSections(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the ZIP sections comprising the output APK have been output.
+ *
+ * <p>The provided data sources are guaranteed to not be used by the engine after this method
+ * terminates.
+ *
+ * @param zipEntries the section of ZIP archive containing Local File Header records and data of
+ * the ZIP entries. In a well-formed archive, this section starts at the start of the
+ * archive and extends all the way to the ZIP Central Directory.
+ * @param zipCentralDirectory ZIP Central Directory section
+ * @param zipEocd ZIP End of Central Directory (EoCD) record
+ *
+ * @return request to add an APK Signing Block to the output or {@code null} if the output must
+ * not contain an APK Signing Block. The request must be fulfilled before
+ * {@link #outputDone()} is invoked.
+ *
+ * @throws IOException if an I/O error occurs while reading the provided ZIP sections
+ * @throws ApkFormatException if the provided APK is malformed in a way which prevents this
+ * engine from producing a valid signature. For example, if the APK Signing Block
+ * provided to the engine is malformed.
+ * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+ * cryptographic algorithm implementation is missing
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws SignatureException if an error occurred while generating a signature
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries or to output JAR signature, or if the engine is closed
+ */
+ OutputApkSigningBlockRequest2 outputZipSections2(
+ DataSource zipEntries,
+ DataSource zipCentralDirectory,
+ DataSource zipEocd)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ InvalidKeyException, SignatureException, IllegalStateException;
+
+ /**
+ * Indicates to this engine that the signed APK was output.
+ *
+ * <p>This does not change the output APK. The method helps the client confirm that the current
+ * output is signed.
+ *
+ * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
+ * entries or to output signatures, or if the engine is closed
+ */
+ void outputDone() throws IllegalStateException;
+
+ /**
+ * Generates a V4 signature proto and write to output file.
+ *
+ * @param data Input data to calculate a verity hash tree and hash root
+ * @param outputFile To store the serialized V4 Signature.
+ * @param ignoreFailures Whether any failures will be silently ignored.
+ * @throws InvalidKeyException if a signature could not be generated because a signing key is
+ * not suitable for generating the signature
+ * @throws NoSuchAlgorithmException if a signature could not be generated because a required
+ * cryptographic algorithm implementation is missing
+ * @throws SignatureException if an error occurred while generating a signature
+ * @throws IOException if protobuf fails to be serialized and written to file
+ */
+ void signV4(DataSource data, File outputFile, boolean ignoreFailures)
+ throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
+
+ /**
+ * Checks if the signing configuration provided to the engine is capable of creating a
+ * SourceStamp.
+ */
+ default boolean isEligibleForSourceStamp() {
+ return false;
+ }
+
+ /** Generates the digest of the certificate used to sign the source stamp. */
+ default byte[] generateSourceStampCertificateDigest() throws SignatureException {
+ return new byte[0];
+ }
+
+ /**
+ * Indicates to this engine that it will no longer be used. Invoking this on an already closed
+ * engine is OK.
+ *
+ * <p>This does not change the output APK. For example, if the output APK is not yet fully
+ * signed, it will remain so after this method terminates.
+ */
+ @Override
+ void close();
+
+ /**
+ * Instructions about how to handle an input APK's JAR entry.
+ *
+ * <p>The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
+ * may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
+ * which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
+ * invoked.
+ */
+ public static class InputJarEntryInstructions {
+ private final OutputPolicy mOutputPolicy;
+ private final InspectJarEntryRequest mInspectJarEntryRequest;
+
+ /**
+ * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
+ * output policy and without a request to inspect the entry.
+ */
+ public InputJarEntryInstructions(OutputPolicy outputPolicy) {
+ this(outputPolicy, null);
+ }
+
+ /**
+ * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
+ * output mode and with the provided request to inspect the entry.
+ *
+ * @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
+ * need to inspect the entry.
+ */
+ public InputJarEntryInstructions(
+ OutputPolicy outputPolicy,
+ InspectJarEntryRequest inspectJarEntryRequest) {
+ mOutputPolicy = outputPolicy;
+ mInspectJarEntryRequest = inspectJarEntryRequest;
+ }
+
+ /**
+ * Returns the output policy for this entry.
+ */
+ public OutputPolicy getOutputPolicy() {
+ return mOutputPolicy;
+ }
+
+ /**
+ * Returns the request to inspect the JAR entry or {@code null} if there is no need to
+ * inspect the entry.
+ */
+ public InspectJarEntryRequest getInspectJarEntryRequest() {
+ return mInspectJarEntryRequest;
+ }
+
+ /**
+ * Output policy for an input APK's JAR entry.
+ */
+ public static enum OutputPolicy {
+ /** Entry must not be output. */
+ SKIP,
+
+ /** Entry should be output. */
+ OUTPUT,
+
+ /** Entry will be output by the engine. The client can thus ignore this input entry. */
+ OUTPUT_BY_ENGINE,
+ }
+ }
+
+ /**
+ * Request to inspect the specified JAR entry.
+ *
+ * <p>The entry's uncompressed data must be provided to the data sink returned by
+ * {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
+ * must be invoked.
+ */
+ interface InspectJarEntryRequest {
+
+ /**
+ * Returns the data sink into which the entry's uncompressed data should be sent.
+ */
+ DataSink getDataSink();
+
+ /**
+ * Indicates that entry's data has been provided in full.
+ */
+ void done();
+
+ /**
+ * Returns the name of the JAR entry.
+ */
+ String getEntryName();
+ }
+
+ /**
+ * Request to add JAR signature (aka v1 signature) to the output APK.
+ *
+ * <p>Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
+ * which {@link #done()} must be invoked.
+ */
+ interface OutputJarSignatureRequest {
+
+ /**
+ * Returns JAR entries that must be added to the output APK.
+ */
+ List<JarEntry> getAdditionalJarEntries();
+
+ /**
+ * Indicates that the JAR entries contained in this request were added to the output APK.
+ */
+ void done();
+
+ /**
+ * JAR entry.
+ */
+ public static class JarEntry {
+ private final String mName;
+ private final byte[] mData;
+
+ /**
+ * Constructs a new {@code JarEntry} with the provided name and data.
+ *
+ * @param data uncompressed data of the entry. Changes to this array will not be
+ * reflected in {@link #getData()}.
+ */
+ public JarEntry(String name, byte[] data) {
+ mName = name;
+ mData = data.clone();
+ }
+
+ /**
+ * Returns the name of this ZIP entry.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the uncompressed data of this JAR entry.
+ */
+ public byte[] getData() {
+ return mData.clone();
+ }
+ }
+ }
+
+ /**
+ * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
+ * signature(s) of the APK are contained in this block.
+ *
+ * <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
+ * output APK such that the block is immediately before the ZIP Central Directory, the offset of
+ * ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
+ * accordingly, and then {@link #done()} must be invoked.
+ *
+ * <p>If the output contains an APK Signing Block, that block must be replaced by the block
+ * contained in this request.
+ *
+ * @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}.
+ */
+ @Deprecated
+ interface OutputApkSigningBlockRequest {
+
+ /**
+ * Returns the APK Signing Block.
+ */
+ byte[] getApkSigningBlock();
+
+ /**
+ * Indicates that the APK Signing Block was output as requested.
+ */
+ void done();
+ }
+
+ /**
+ * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
+ * signature(s) of the APK are contained in this block.
+ *
+ * <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
+ * output APK such that the block is immediately before the ZIP Central Directory. Immediately
+ * before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by
+ * {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the
+ * ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()}
+ * must be invoked.
+ *
+ * <p>If the output contains an APK Signing Block, that block must be replaced by the block
+ * contained in this request.
+ */
+ interface OutputApkSigningBlockRequest2 {
+ /**
+ * Returns the APK Signing Block.
+ */
+ byte[] getApkSigningBlock();
+
+ /**
+ * Indicates that the APK Signing Block was output as requested.
+ */
+ void done();
+
+ /**
+ * Returns the number of 0x00 bytes the caller must place immediately before APK Signing
+ * Block.
+ */
+ int getPaddingSizeBeforeApkSigningBlock();
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerificationIssue.java b/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerificationIssue.java
new file mode 100644
index 0000000000..fa2b7aa58c
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerificationIssue.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+/**
+ * This class is intended as a lightweight representation of an APK signature verification issue
+ * where the client does not require the additional textual details provided by a subclass.
+ */
+public class ApkVerificationIssue {
+ /* The V2 signer(s) could not be read from the V2 signature block */
+ public static final int V2_SIG_MALFORMED_SIGNERS = 1;
+ /* A V2 signature block exists without any V2 signers */
+ public static final int V2_SIG_NO_SIGNERS = 2;
+ /* Failed to parse a signer's block in the V2 signature block */
+ public static final int V2_SIG_MALFORMED_SIGNER = 3;
+ /* Failed to parse the signer's signature record in the V2 signature block */
+ public static final int V2_SIG_MALFORMED_SIGNATURE = 4;
+ /* The V2 signer contained no signatures */
+ public static final int V2_SIG_NO_SIGNATURES = 5;
+ /* The V2 signer's certificate could not be parsed */
+ public static final int V2_SIG_MALFORMED_CERTIFICATE = 6;
+ /* No signing certificates exist for the V2 signer */
+ public static final int V2_SIG_NO_CERTIFICATES = 7;
+ /* Failed to parse the V2 signer's digest record */
+ public static final int V2_SIG_MALFORMED_DIGEST = 8;
+ /* The V3 signer(s) could not be read from the V3 signature block */
+ public static final int V3_SIG_MALFORMED_SIGNERS = 9;
+ /* A V3 signature block exists without any V3 signers */
+ public static final int V3_SIG_NO_SIGNERS = 10;
+ /* Failed to parse a signer's block in the V3 signature block */
+ public static final int V3_SIG_MALFORMED_SIGNER = 11;
+ /* Failed to parse the signer's signature record in the V3 signature block */
+ public static final int V3_SIG_MALFORMED_SIGNATURE = 12;
+ /* The V3 signer contained no signatures */
+ public static final int V3_SIG_NO_SIGNATURES = 13;
+ /* The V3 signer's certificate could not be parsed */
+ public static final int V3_SIG_MALFORMED_CERTIFICATE = 14;
+ /* No signing certificates exist for the V3 signer */
+ public static final int V3_SIG_NO_CERTIFICATES = 15;
+ /* Failed to parse the V3 signer's digest record */
+ public static final int V3_SIG_MALFORMED_DIGEST = 16;
+ /* The source stamp signer contained no signatures */
+ public static final int SOURCE_STAMP_NO_SIGNATURE = 17;
+ /* The source stamp signer's certificate could not be parsed */
+ public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18;
+ /* The source stamp contains a signature produced using an unknown algorithm */
+ public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19;
+ /* Failed to parse the signer's signature in the source stamp signature block */
+ public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20;
+ /* The source stamp's signature block failed verification */
+ public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21;
+ /* An exception was encountered when verifying the source stamp */
+ public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22;
+ /* The certificate digest in the APK does not match the expected digest */
+ public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23;
+ /*
+ * The APK contains a source stamp signature block without a corresponding stamp certificate
+ * digest in the APK contents.
+ */
+ public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24;
+ /*
+ * The APK does not contain the source stamp certificate digest file nor the source stamp
+ * signature block.
+ */
+ public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25;
+ /*
+ * None of the signatures provided by the source stamp were produced with a known signature
+ * algorithm.
+ */
+ public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26;
+ /*
+ * The source stamp signer's certificate in the signing block does not match the certificate in
+ * the APK.
+ */
+ public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27;
+ /* The APK could not be properly parsed due to a ZIP or APK format exception */
+ public static final int MALFORMED_APK = 28;
+ /* An unexpected exception was caught when attempting to verify the APK's signatures */
+ public static final int UNEXPECTED_EXCEPTION = 29;
+ /* The APK contains the certificate digest file but does not contain a stamp signature block */
+ public static final int SOURCE_STAMP_SIG_MISSING = 30;
+ /* Source stamp block contains a malformed attribute. */
+ public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31;
+ /* Source stamp block contains an unknown attribute. */
+ public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32;
+ /**
+ * Failed to parse the SigningCertificateLineage structure in the source stamp
+ * attributes section.
+ */
+ public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33;
+ /**
+ * The source stamp certificate does not match the terminal node in the provided
+ * proof-of-rotation structure describing the stamp certificate history.
+ */
+ public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34;
+ /**
+ * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
+ * with signature(s) that did not verify.
+ */
+ public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
+ /** No V1 / jar signing signature blocks were found in the APK. */
+ public static final int JAR_SIG_NO_SIGNATURES = 36;
+ /** An exception was encountered when parsing the V1 / jar signer in the signature block. */
+ public static final int JAR_SIG_PARSE_EXCEPTION = 37;
+ /** The source stamp timestamp attribute has an invalid value. */
+ public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38;
+
+ private final int mIssueId;
+ private final String mFormat;
+ private final Object[] mParams;
+
+ /**
+ * Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and
+ * {@code params}.
+ */
+ public ApkVerificationIssue(String format, Object... params) {
+ mIssueId = -1;
+ mFormat = format;
+ mParams = params;
+ }
+
+ /**
+ * Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code
+ * params}.
+ */
+ public ApkVerificationIssue(int issueId, Object... params) {
+ mIssueId = issueId;
+ mFormat = null;
+ mParams = params;
+ }
+
+ /**
+ * Returns the numeric ID for this issue.
+ */
+ public int getIssueId() {
+ return mIssueId;
+ }
+
+ /**
+ * Returns the optional parameters for this issue.
+ */
+ public Object[] getParams() {
+ return mParams;
+ }
+
+ @Override
+ public String toString() {
+ // If this instance was created by a subclass with a format string then return the same
+ // formatted String as the subclass.
+ if (mFormat != null) {
+ return String.format(mFormat, mParams);
+ }
+ StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId);
+ for (Object param : mParams) {
+ result.append(", ").append(param.toString());
+ }
+ return result.toString();
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerifier.java
new file mode 100644
index 0000000000..50b3d9f5e2
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerifier.java
@@ -0,0 +1,3657 @@
+/*
+ * 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 static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes;
+import static com.android.apksig.apk.ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest;
+import static com.android.apksig.apk.ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_SOURCE_STAMP;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+
+import com.android.apksig.ApkVerifier.Result.V2SchemeSignerInfo;
+import com.android.apksig.ApkVerifier.Result.V3SchemeSignerInfo;
+import com.android.apksig.SigningCertificateLineage.SignerConfig;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.Result.SignerInfo.ContentDigest;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.SignatureNotFoundException;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
+import com.android.apksig.internal.apk.v4.V4SchemeVerifier;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+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.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+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.EnumMap;
+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.
+ *
+ * <p>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.
+ *
+ * <p>Use {@link Builder} to obtain instances of this verifier.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
+ */
+public class ApkVerifier {
+
+ private static final Set<Issue> LINEAGE_RELATED_ISSUES = new HashSet<>(Arrays.asList(
+ Issue.V3_SIG_MALFORMED_LINEAGE, Issue.V3_INCONSISTENT_LINEAGES,
+ Issue.V3_SIG_POR_DID_NOT_VERIFY, Issue.V3_SIG_POR_CERT_MISMATCH));
+
+ private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
+ loadSupportedApkSigSchemeNames();
+
+ private static Map<Integer, String> loadSupportedApkSigSchemeNames() {
+ Map<Integer, String> 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 File mV4SignatureFile;
+
+ private final Integer mMinSdkVersion;
+ private final int mMaxSdkVersion;
+
+ private ApkVerifier(
+ File apkFile,
+ DataSource apkDataSource,
+ File v4SignatureFile,
+ Integer minSdkVersion,
+ int maxSdkVersion) {
+ mApkFile = apkFile;
+ mApkDataSource = apkDataSource;
+ mV4SignatureFile = v4SignatureFile;
+ 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.
+ *
+ * <p>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 {
+ 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 = verifyAndGetMinSdkVersion(apk, zipSections);
+
+ Result result = new Result();
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+ new HashMap<>();
+
+ // 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<Integer, String> supportedSchemeNames = getSupportedSchemeNames(maxSdkVersion);
+
+ // 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<Integer> foundApkSigSchemeIds = new HashSet<>(2);
+ if (maxSdkVersion >= AndroidSdkVersion.N) {
+ RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED;
+ // Android T and newer attempts to verify APKs using APK Signature Scheme V3.1. v3.0
+ // also includes stripping protection for the minimum SDK version on which the rotated
+ // signing key should be used.
+ int rotationMinSdkVersion = 0;
+ if (maxSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) {
+ try {
+ ApkSigningBlockUtils.Result v31Result = new V3SchemeVerifier.Builder(apk,
+ zipSections, Math.max(minSdkVersion, MIN_SDK_WITH_V31_SUPPORT),
+ maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+ .build()
+ .verify();
+ foundApkSigSchemeIds.add(VERSION_APK_SIGNATURE_SCHEME_V31);
+ rotationMinSdkVersion = v31Result.signers.stream().mapToInt(
+ signer -> signer.minSdkVersion).min().orElse(0);
+ result.mergeFrom(v31Result);
+ signatureSchemeApkContentDigests.put(
+ VERSION_APK_SIGNATURE_SCHEME_V31,
+ getApkContentDigestsFromSigningSchemeResult(v31Result));
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // v3.1 signature not required
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+ // Android P and newer attempts to verify APKs using APK Signature Scheme v3; since a
+ // V3.1 block should only be written with a V3.0 block, always perform the V3.0 check
+ // if the minSdkVersion supports V3.0.
+ if (maxSdkVersion >= AndroidSdkVersion.P) {
+ try {
+ V3SchemeVerifier.Builder builder = new V3SchemeVerifier.Builder(apk,
+ zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P),
+ maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ if (rotationMinSdkVersion > 0) {
+ builder.setRotationMinSdkVersion(rotationMinSdkVersion);
+ }
+ ApkSigningBlockUtils.Result v3Result = builder.build().verify();
+ foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ result.mergeFrom(v3Result);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3,
+ getApkContentDigestsFromSigningSchemeResult(v3Result));
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // v3 signature not required unless a v3.1 signature was found as a v3.1
+ // signature is intended to support key rotation on T+ with the v3 signature
+ // containing the original signing key.
+ if (foundApkSigSchemeIds.contains(
+ VERSION_APK_SIGNATURE_SCHEME_V31)) {
+ result.addError(Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK);
+ }
+ }
+ 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);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2,
+ getApkContentDigestsFromSigningSchemeResult(v2Result));
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // v2 signature not required
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+
+ // If v4 file is specified, use additional verification on it
+ if (mV4SignatureFile != null) {
+ final ApkSigningBlockUtils.Result v4Result =
+ V4SchemeVerifier.verify(apk, mV4SignatureFile);
+ foundApkSigSchemeIds.add(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+ result.mergeFrom(v4Result);
+ 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);
+ }
+ }
+ }
+
+ List<CentralDirectoryRecord> cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+
+ // 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);
+ signatureSchemeApkContentDigests.put(
+ ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME,
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // Verify the SourceStamp, if found in the APK.
+ try {
+ CentralDirectoryRecord sourceStampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
+ cdRecord.getName())) {
+ sourceStampCdRecord = cdRecord;
+ break;
+ }
+ }
+ // If SourceStamp file is found inside the APK, there must be a SourceStamp
+ // block in the APK signing block as well.
+ if (sourceStampCdRecord != null) {
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ ApkSigResult sourceStampResult =
+ V2SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ Math.max(minSdkVersion, AndroidSdkVersion.R),
+ maxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ }
+ } catch (SignatureNotFoundException ignored) {
+ result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ 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<Result.V1SchemeSignerInfo> v1Signers =
+ new ArrayList<>(result.getV1SchemeSigners());
+ ArrayList<Result.V2SchemeSignerInfo> v2Signers =
+ new ArrayList<>(result.getV2SchemeSigners());
+ ArrayList<ByteArray> v1SignerCerts = new ArrayList<>();
+ ArrayList<ByteArray> v2SignerCerts = new ArrayList<>();
+ for (Result.V1SchemeSignerInfo signer : v1Signers) {
+ try {
+ v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded()));
+ } catch (CertificateEncodingException e) {
+ throw new IllegalStateException(
+ "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 IllegalStateException(
+ "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<Result.V1SchemeSignerInfo> 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<Result.V2SchemeSignerInfo> 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<Result.V3SchemeSignerInfo> 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 there is a v4 scheme signer, make sure that their certificates match.
+ // The apkDigest field in the v4 signature should match the selected v2/v3.
+ if (result.isVerifiedUsingV4Scheme()) {
+ List<Result.V4SchemeSignerInfo> v4Signers = result.getV4SchemeSigners();
+
+ List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV4 =
+ v4Signers.get(0).getContentDigests();
+ if (digestsFromV4.size() != 1) {
+ result.addError(Issue.V4_SIG_UNEXPECTED_DIGESTS, digestsFromV4.size());
+ if (digestsFromV4.isEmpty()) {
+ return result;
+ }
+ }
+ final byte[] digestFromV4 = digestsFromV4.get(0).getValue();
+
+ if (result.isVerifiedUsingV3Scheme()) {
+ final boolean isV31 = result.isVerifiedUsingV31Scheme();
+ final int expectedSize = isV31 ? 2 : 1;
+ if (v4Signers.size() != expectedSize) {
+ result.addError(isV31 ? Issue.V41_SIG_NEEDS_TWO_SIGNERS
+ : Issue.V4_SIG_MULTIPLE_SIGNERS);
+ return result;
+ }
+
+ checkV4Signer(result.getV3SchemeSigners(), v4Signers.get(0).mCerts, digestFromV4,
+ result);
+ if (isV31) {
+ List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV41 =
+ v4Signers.get(1).getContentDigests();
+ if (digestsFromV41.size() != 1) {
+ result.addError(Issue.V4_SIG_UNEXPECTED_DIGESTS, digestsFromV41.size());
+ if (digestsFromV41.isEmpty()) {
+ return result;
+ }
+ }
+ final byte[] digestFromV41 = digestsFromV41.get(0).getValue();
+ checkV4Signer(result.getV31SchemeSigners(), v4Signers.get(1).mCerts,
+ digestFromV41, result);
+ }
+ } else if (result.isVerifiedUsingV2Scheme()) {
+ if (v4Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners();
+ if (v2Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ // Compare certificates.
+ checkV4Certificate(v4Signers.get(0).mCerts, v2Signers.get(0).mCerts, result);
+
+ // Compare digests.
+ final byte[] digestFromV2 = pickBestDigestForV4(
+ v2Signers.get(0).getContentDigests());
+ if (!Arrays.equals(digestFromV4, digestFromV2)) {
+ result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH, 2, toHex(digestFromV2),
+ toHex(digestFromV4));
+ }
+ } else {
+ throw new RuntimeException("V4 signature must be also verified with V2/V3");
+ }
+ }
+
+ // If the targetSdkVersion has a minimum required signature scheme version then verify
+ // that the APK was signed with at least that version.
+ try {
+ if (androidManifest == null) {
+ androidManifest = getAndroidManifestFromApk(apk, zipSections);
+ }
+ } catch (ApkFormatException e) {
+ // If the manifest is not available then skip the minimum signature scheme requirement
+ // to support bundle verification.
+ }
+ if (androidManifest != null) {
+ int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest(
+ androidManifest.slice());
+ int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion);
+ // The platform currently only enforces a single minimum signature scheme version, but
+ // when later platform versions support another minimum version this will need to be
+ // expanded to verify the minimum based on the target and maximum SDK version.
+ if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME
+ && maxSdkVersion >= targetSdkVersion) {
+ switch (minSchemeVersion) {
+ case VERSION_APK_SIGNATURE_SCHEME_V2:
+ if (result.isVerifiedUsingV2Scheme()) {
+ break;
+ }
+ // Allow this case to fall through to the next as a signature satisfying a
+ // later scheme version will also satisfy this requirement.
+ case VERSION_APK_SIGNATURE_SCHEME_V3:
+ if (result.isVerifiedUsingV3Scheme() || result.isVerifiedUsingV31Scheme()) {
+ break;
+ }
+ result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET,
+ targetSdkVersion,
+ minSchemeVersion);
+ }
+ }
+ }
+
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // Verified
+ result.setVerified();
+ if (result.isVerifiedUsingV31Scheme()) {
+ List<Result.V3SchemeSignerInfo> v31Signers = result.getV31SchemeSigners();
+ result.addSignerCertificate(v31Signers.get(v31Signers.size() - 1).getCertificate());
+ } else if (result.isVerifiedUsingV3Scheme()) {
+ List<Result.V3SchemeSignerInfo> 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 v3 schemes");
+ }
+
+ return result;
+ }
+
+ /**
+ * Verifies and returns the minimum SDK version, either as provided to the builder or as read
+ * from the {@code apk}'s AndroidManifest.xml.
+ */
+ private int verifyAndGetMinSdkVersion(DataSource apk, ApkUtils.ZipSections zipSections)
+ throws ApkFormatException, IOException {
+ 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
+ + ")");
+ }
+ return mMinSdkVersion;
+ }
+
+ ByteBuffer androidManifest = null;
+ // Need to obtain minSdkVersion from the APK's AndroidManifest.xml
+ if (androidManifest == null) {
+ androidManifest = getAndroidManifestFromApk(apk, zipSections);
+ }
+ int minSdkVersion =
+ ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice());
+ if (minSdkVersion > mMaxSdkVersion) {
+ throw new IllegalArgumentException(
+ "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion ("
+ + mMaxSdkVersion + ")");
+ }
+ return minSdkVersion;
+ }
+
+ /**
+ * Returns the mapping of signature scheme version to signature scheme name for all signature
+ * schemes starting from V2 supported by the {@code maxSdkVersion}.
+ */
+ private static Map<Integer, String> getSupportedSchemeNames(int maxSdkVersion) {
+ Map<Integer, String> 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.emptyMap();
+ }
+ return supportedSchemeNames;
+ }
+
+ /**
+ * Verifies the APK's source stamp signature and returns the result of the verification.
+ *
+ * <p>The APK's source stamp can be considered verified if the result's {@link
+ * Result#isVerified} returns {@code true}. The details of the source stamp verification can
+ * be obtained from the result's {@link Result#getSourceStampInfo()}} including the success or
+ * failure cause from {@link Result.SourceStampInfo#getSourceStampVerificationStatus()}. If the
+ * verification fails additional details regarding the failure can be obtained from {@link
+ * Result#getAllErrors()}}.
+ */
+ public Result verifySourceStamp() {
+ return verifySourceStamp(null);
+ }
+
+ /**
+ * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
+ * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
+ * of the verification.
+ *
+ * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
+ * if present, without verifying the actual source stamp certificate used to sign the source
+ * stamp. This can be used to verify an APK contains a properly signed source stamp without
+ * verifying a particular signer.
+ *
+ * @see #verifySourceStamp()
+ */
+ public Result verifySourceStamp(String expectedCertDigest) {
+ Closeable in = null;
+ try {
+ DataSource apk;
+ if (mApkDataSource != null) {
+ apk = mApkDataSource;
+ } else if (mApkFile != null) {
+ RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+ in = f;
+ apk = DataSources.asDataSource(f, 0, f.length());
+ } else {
+ throw new IllegalStateException("APK not provided");
+ }
+ return verifySourceStamp(apk, expectedCertDigest);
+ } catch (IOException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ Issue.UNEXPECTED_EXCEPTION, e);
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Compares the digests coming from signature blocks. Returns {@code true} if at least one
+ * digest algorithm is present in both digests and actual digests for all common algorithms
+ * are the same.
+ */
+ public static boolean compareDigests(
+ Map<ContentDigestAlgorithm, byte[]> firstDigests,
+ Map<ContentDigestAlgorithm, byte[]> secondDigests) throws NoSuchAlgorithmException {
+
+ Set<ContentDigestAlgorithm> intersectKeys = new HashSet<>(firstDigests.keySet());
+ intersectKeys.retainAll(secondDigests.keySet());
+ if (intersectKeys.isEmpty()) {
+ return false;
+ }
+
+ for (ContentDigestAlgorithm algorithm : intersectKeys) {
+ if (!Arrays.equals(firstDigests.get(algorithm),
+ secondDigests.get(algorithm))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+ /**
+ * Verifies the provided {@code apk}'s source stamp signature, including verification of the
+ * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
+ * returns the result of the verification.
+ *
+ * @see #verifySourceStamp(String)
+ */
+ private Result verifySourceStamp(DataSource apk, String expectedCertDigest) {
+ try {
+ ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+ int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections);
+
+ // Attempt to obtain the source stamp's certificate digest from the APK.
+ List<CentralDirectoryRecord> cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+ CentralDirectoryRecord sourceStampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+ sourceStampCdRecord = cdRecord;
+ break;
+ }
+ }
+
+ // If the source stamp's certificate digest is not available within the APK then the
+ // source stamp cannot be verified; check if a source stamp signing block is in the
+ // APK's signature block to determine the appropriate status to return.
+ if (sourceStampCdRecord == null) {
+ boolean stampSigningBlockFound;
+ try {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ VERSION_SOURCE_STAMP);
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID, result);
+ stampSigningBlockFound = true;
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+ stampSigningBlockFound = false;
+ }
+ if (stampSigningBlockFound) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
+ Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST);
+ } else {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_MISSING,
+ Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ }
+ }
+
+ // Verify that the contents of the source stamp certificate digest match the expected
+ // value, if provided.
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ if (expectedCertDigest != null) {
+ String actualCertDigest = ApkSigningBlockUtils.toHex(sourceStampCertificateDigest);
+ if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus
+ .CERT_DIGEST_MISMATCH,
+ Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, actualCertDigest,
+ expectedCertDigest);
+ }
+ }
+
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+ new HashMap<>();
+ Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(mMaxSdkVersion);
+ Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
+
+ Result result = new Result();
+ ApkSigningBlockUtils.Result v3Result = null;
+ if (mMaxSdkVersion >= AndroidSdkVersion.P) {
+ v3Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds,
+ supportedSchemeNames, signatureSchemeApkContentDigests,
+ VERSION_APK_SIGNATURE_SCHEME_V3,
+ Math.max(minSdkVersion, AndroidSdkVersion.P));
+ if (v3Result != null && v3Result.containsErrors()) {
+ result.mergeFrom(v3Result);
+ return mergeSourceStampResult(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ result);
+ }
+ }
+
+ ApkSigningBlockUtils.Result v2Result = null;
+ if (mMaxSdkVersion >= AndroidSdkVersion.N && (minSdkVersion < AndroidSdkVersion.P
+ || foundApkSigSchemeIds.isEmpty())) {
+ v2Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds,
+ supportedSchemeNames, signatureSchemeApkContentDigests,
+ VERSION_APK_SIGNATURE_SCHEME_V2,
+ Math.max(minSdkVersion, AndroidSdkVersion.N));
+ if (v2Result != null && v2Result.containsErrors()) {
+ result.mergeFrom(v2Result);
+ return mergeSourceStampResult(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ result);
+ }
+ }
+
+ if (minSdkVersion < AndroidSdkVersion.N || foundApkSigSchemeIds.isEmpty()) {
+ signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections));
+ }
+
+ ApkSigResult sourceStampResult =
+ V2SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ minSdkVersion,
+ mMaxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ // Since the caller is only seeking to verify the source stamp the Result can be marked
+ // as verified if the source stamp verification was successful.
+ if (sourceStampResult.verified) {
+ result.setVerified();
+ } else {
+ // To prevent APK signature verification with a failed / missing source stamp the
+ // source stamp verification will only log warnings; to allow the caller to capture
+ // the failure reason treat all warnings as errors.
+ result.setWarningsAsErrors(true);
+ }
+ return result;
+ } catch (ApkFormatException | IOException | ZipFormatException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ Issue.MALFORMED_APK, e);
+ } catch (NoSuchAlgorithmException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR,
+ Issue.UNEXPECTED_EXCEPTION, e);
+ } catch (SignatureNotFoundException e) {
+ return createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED,
+ Issue.SOURCE_STAMP_SIG_MISSING);
+ }
+ }
+
+ /**
+ * Creates and returns a {@code Result} that can be returned for source stamp verification
+ * with the provided source stamp {@code verificationStatus}, and logs an error for the
+ * specified {@code issue} and {@code params}.
+ */
+ private static Result createSourceStampResultWithError(
+ Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, Issue issue,
+ Object... params) {
+ Result result = new Result();
+ result.addError(issue, params);
+ return mergeSourceStampResult(verificationStatus, result);
+ }
+
+ /**
+ * Creates a new {@link Result.SourceStampInfo} under the provided {@code result} and sets the
+ * source stamp status to the provided {@code verificationStatus}.
+ */
+ private static Result mergeSourceStampResult(
+ Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus,
+ Result result) {
+ result.mSourceStampInfo = new Result.SourceStampInfo(verificationStatus);
+ return result;
+ }
+
+ /**
+ * Gets content digests, signing lineage and certificates from the given {@code schemeId} block
+ * alongside encountered errors info and creates a new {@code Result} containing all this
+ * information.
+ */
+ public static Result getSigningBlockResult(
+ DataSource apk, ApkUtils.ZipSections zipSections, int sdkVersion, int schemeId)
+ throws IOException, NoSuchAlgorithmException{
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests =
+ new HashMap<>();
+ Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(sdkVersion);
+ Set<Integer> foundApkSigSchemeIds = new HashSet<>(2);
+
+ Result result = new Result();
+ result.mergeFrom(getApkContentDigests(apk, zipSections,
+ foundApkSigSchemeIds, supportedSchemeNames, sigSchemeApkContentDigests,
+ schemeId, sdkVersion, sdkVersion));
+ return result;
+ }
+
+ /**
+ * Gets the content digest from the {@code result}'s signers. Ignores {@code ContentDigest}s
+ * for which {@code SignatureAlgorithm} is {@code null}.
+ */
+ public static Map<ContentDigestAlgorithm, byte[]> getContentDigestsFromResult(
+ Result result, int schemeId) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+ if (!(schemeId == VERSION_APK_SIGNATURE_SCHEME_V2
+ || schemeId == VERSION_APK_SIGNATURE_SCHEME_V3
+ || schemeId == VERSION_APK_SIGNATURE_SCHEME_V31)) {
+ return apkContentDigests;
+ }
+ switch (schemeId) {
+ case VERSION_APK_SIGNATURE_SCHEME_V2:
+ for (V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
+ getContentDigests(signerInfo.getContentDigests(), apkContentDigests);
+ }
+ break;
+ case VERSION_APK_SIGNATURE_SCHEME_V3:
+ for (Result.V3SchemeSignerInfo signerInfo : result.getV3SchemeSigners()) {
+ getContentDigests(signerInfo.getContentDigests(), apkContentDigests);
+ }
+ break;
+ case VERSION_APK_SIGNATURE_SCHEME_V31:
+ for (Result.V3SchemeSignerInfo signerInfo : result.getV31SchemeSigners()) {
+ getContentDigests(signerInfo.getContentDigests(), apkContentDigests);
+ }
+ break;
+ }
+ return apkContentDigests;
+ }
+
+ private static void getContentDigests(
+ List<ContentDigest> digests, Map<ContentDigestAlgorithm, byte[]> contentDigestsMap) {
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest :
+ digests) {
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(
+ contentDigest.getSignatureAlgorithmId());
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ contentDigestsMap.put(signatureAlgorithm.getContentDigestAlgorithm(),
+ contentDigest.getValue());
+ }
+ }
+
+ /**
+ * Checks whether a given {@code result} contains errors indicating that a signing certificate
+ * lineage is incorrect.
+ */
+ public static boolean containsLineageErrors(
+ Result result) {
+ if (!result.containsErrors()) {
+ return false;
+ }
+
+ return (result.getAllErrors().stream().map(i -> i.getIssue())
+ .anyMatch(error -> LINEAGE_RELATED_ISSUES.contains(error)));
+ }
+
+
+ /**
+ * Gets a lineage from the first signer from a given {@code result}.
+ * If the {@code result} contains errors related to the lineage incorrectness or there are no
+ * signers or certificates, it returns {@code null}.
+ * If the lineage is empty but there is a signer, it returns a 1-element lineage containing
+ * the signing key.
+ */
+ public static SigningCertificateLineage getLineageFromResult(
+ Result result, int sdkVersion, int schemeId)
+ throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ if (!(schemeId == VERSION_APK_SIGNATURE_SCHEME_V3
+ || schemeId == VERSION_APK_SIGNATURE_SCHEME_V31)
+ || containsLineageErrors(result)) {
+ return null;
+ }
+ List<V3SchemeSignerInfo> signersInfo =
+ schemeId == VERSION_APK_SIGNATURE_SCHEME_V3 ?
+ result.getV3SchemeSigners() : result.getV31SchemeSigners();
+ if (signersInfo.isEmpty()) {
+ return null;
+ }
+ V3SchemeSignerInfo firstSignerInfo = signersInfo.get(0);
+ SigningCertificateLineage lineage = firstSignerInfo.mSigningCertificateLineage;
+ if (lineage == null && firstSignerInfo.getCertificate() != null) {
+ try {
+ lineage = new SigningCertificateLineage.Builder(
+ new SignerConfig.Builder(
+ /* privateKey= */ null, firstSignerInfo.getCertificate())
+ .build()).build();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ return lineage;
+ }
+
+ /**
+ * Obtains the APK content digest(s) and adds them to the provided {@code
+ * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be
+ * merged with a {@code Result} to notify the client of any errors.
+ *
+ * <p>Note, this method currently only supports signature scheme V2 and V3; to obtain the
+ * content digests for V1 signatures use {@link
+ * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a
+ * signature scheme version other than V2 or V3 is provided a {@code null} value will be
+ * returned.
+ */
+ private ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk,
+ ApkUtils.ZipSections zipSections, Set<Integer> foundApkSigSchemeIds,
+ Map<Integer, String> supportedSchemeNames,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests,
+ int apkSigSchemeVersion, int minSdkVersion)
+ throws IOException, NoSuchAlgorithmException {
+ return getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, supportedSchemeNames,
+ sigSchemeApkContentDigests, apkSigSchemeVersion, minSdkVersion, mMaxSdkVersion);
+ }
+
+
+ /**
+ * Obtains the APK content digest(s) and adds them to the provided {@code
+ * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be
+ * merged with a {@code Result} to notify the client of any errors.
+ *
+ * <p>Note, this method currently only supports signature scheme V2 and V3; to obtain the
+ * content digests for V1 signatures use {@link
+ * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a
+ * signature scheme version other than V2 or V3 is provided a {@code null} value will be
+ * returned.
+ */
+ private static ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk,
+ ApkUtils.ZipSections zipSections, Set<Integer> foundApkSigSchemeIds,
+ Map<Integer, String> supportedSchemeNames,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests,
+ int apkSigSchemeVersion, int minSdkVersion, int maxSdkVersion)
+ throws IOException, NoSuchAlgorithmException {
+ if (!(apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2
+ || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3
+ || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V31)) {
+ return null;
+ }
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(apkSigSchemeVersion);
+ SignatureInfo signatureInfo;
+ try {
+ int sigSchemeBlockId;
+ switch (apkSigSchemeVersion) {
+ case VERSION_APK_SIGNATURE_SCHEME_V31:
+ sigSchemeBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+ break;
+ case VERSION_APK_SIGNATURE_SCHEME_V3:
+ sigSchemeBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ break;
+ default:
+ sigSchemeBlockId =
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+ }
+ signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections,
+ sigSchemeBlockId, result);
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException e) {
+ return null;
+ }
+ foundApkSigSchemeIds.add(apkSigSchemeVersion);
+
+ Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+ if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) {
+ V2SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
+ contentDigestsToVerify, supportedSchemeNames,
+ foundApkSigSchemeIds, minSdkVersion, maxSdkVersion, result);
+ } else {
+ V3SchemeVerifier.parseSigners(signatureInfo.signatureBlock,
+ contentDigestsToVerify, result);
+ }
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : result.signers) {
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest :
+ signerInfo.contentDigests) {
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(
+ contentDigest.getSignatureAlgorithmId());
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(),
+ contentDigest.getValue());
+ }
+ }
+ sigSchemeApkContentDigests.put(apkSigSchemeVersion, apkContentDigests);
+ return result;
+ }
+
+ private static void checkV4Signer(List<Result.V3SchemeSignerInfo> v3Signers,
+ List<X509Certificate> v4Certs, byte[] digestFromV4, Result result) {
+ if (v3Signers.size() != 1) {
+ result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS);
+ }
+
+ // Compare certificates.
+ checkV4Certificate(v4Certs, v3Signers.get(0).mCerts, result);
+
+ // Compare digests.
+ final byte[] digestFromV3 = pickBestDigestForV4(v3Signers.get(0).getContentDigests());
+ if (!Arrays.equals(digestFromV4, digestFromV3)) {
+ result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH, 3, toHex(digestFromV3),
+ toHex(digestFromV4));
+ }
+ }
+
+ private static void checkV4Certificate(List<X509Certificate> v4Certs,
+ List<X509Certificate> v2v3Certs, Result result) {
+ try {
+ byte[] v4Cert = v4Certs.get(0).getEncoded();
+ byte[] cert = v2v3Certs.get(0).getEncoded();
+ if (!Arrays.equals(cert, v4Cert)) {
+ result.addError(Issue.V4_SIG_V2_V3_SIGNERS_MISMATCH);
+ }
+ } catch (CertificateEncodingException e) {
+ throw new RuntimeException("Failed to encode APK signer cert", e);
+ }
+ }
+
+ private static byte[] pickBestDigestForV4(
+ List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+ collectApkContentDigests(contentDigests, apkContentDigests);
+ return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests);
+ }
+
+ private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestsFromSigningSchemeResult(
+ ApkSigningBlockUtils.Result apkSigningSchemeResult) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+ for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) {
+ collectApkContentDigests(signerInfo.contentDigests, apkContentDigests);
+ }
+ return apkContentDigests;
+ }
+
+ private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
+ List<CentralDirectoryRecord> cdRecords,
+ DataSource apk,
+ ApkUtils.ZipSections zipSections)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord manifestCdRecord = null;
+ Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) {
+ manifestCdRecord = cdRecord;
+ break;
+ }
+ }
+ if (manifestCdRecord == null) {
+ // No JAR signing manifest file found. For SourceStamp verification, returning an empty
+ // digest is enough since this would affect the final digest signed by the stamp, and
+ // thus an empty digest will invalidate that signature.
+ return v1ContentDigest;
+ }
+ try {
+ byte[] manifestBytes =
+ LocalFileRecord.getUncompressedData(
+ apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
+ v1ContentDigest.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
+ return v1ContentDigest;
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ }
+
+ private static void collectApkContentDigests(
+ List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
+ SignatureAlgorithm signatureAlgorithm =
+ SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ ContentDigestAlgorithm contentDigestAlgorithm =
+ signatureAlgorithm.getContentDigestAlgorithm();
+ apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue());
+ }
+
+ }
+
+ private static ByteBuffer getAndroidManifestFromApk(
+ DataSource apk, ApkUtils.ZipSections zipSections)
+ throws IOException, ApkFormatException {
+ List<CentralDirectoryRecord> 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);
+ }
+ }
+
+ private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) {
+ if (targetSdkVersion >= AndroidSdkVersion.R) {
+ return VERSION_APK_SIGNATURE_SCHEME_V2;
+ }
+ return VERSION_JAR_SIGNATURE_SCHEME;
+ }
+
+ /**
+ * 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<IssueWithParams> mErrors = new ArrayList<>();
+ private final List<IssueWithParams> mWarnings = new ArrayList<>();
+ private final List<X509Certificate> mSignerCerts = new ArrayList<>();
+ private final List<V1SchemeSignerInfo> mV1SchemeSigners = new ArrayList<>();
+ private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>();
+ private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
+ private final List<V3SchemeSignerInfo> mV3SchemeSigners = new ArrayList<>();
+ private final List<V3SchemeSignerInfo> mV31SchemeSigners = new ArrayList<>();
+ private final List<V4SchemeSignerInfo> mV4SchemeSigners = new ArrayList<>();
+ private SourceStampInfo mSourceStampInfo;
+
+ private boolean mVerified;
+ private boolean mVerifiedUsingV1Scheme;
+ private boolean mVerifiedUsingV2Scheme;
+ private boolean mVerifiedUsingV3Scheme;
+ private boolean mVerifiedUsingV31Scheme;
+ private boolean mVerifiedUsingV4Scheme;
+ private boolean mSourceStampVerified;
+ private boolean mWarningsAsErrors;
+ 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 {@code true} if the APK's APK Signature Scheme v3.1 signature verified.
+ */
+ public boolean isVerifiedUsingV31Scheme() {
+ return mVerifiedUsingV31Scheme;
+ }
+
+ /**
+ * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified.
+ */
+ public boolean isVerifiedUsingV4Scheme() {
+ return mVerifiedUsingV4Scheme;
+ }
+
+ /**
+ * Returns {@code true} if the APK's SourceStamp signature verified.
+ */
+ public boolean isSourceStampVerified() {
+ return mSourceStampVerified;
+ }
+
+ /**
+ * Returns the verified signers' certificates, one per signer.
+ */
+ public List<X509Certificate> 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<V1SchemeSignerInfo> 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<V1SchemeSignerInfo> getV1SchemeIgnoredSigners() {
+ return mV1SchemeIgnoredSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v2 signers associated with the APK's
+ * signature.
+ */
+ public List<V2SchemeSignerInfo> getV2SchemeSigners() {
+ return mV2SchemeSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v3 signers associated with the APK's
+ * signature.
+ *
+ * <note> Multiple signers represent different targeted platform versions, not
+ * a signing identity of multiple signers. APK Signature Scheme v3 only supports single
+ * signer identities.</note>
+ */
+ public List<V3SchemeSignerInfo> getV3SchemeSigners() {
+ return mV3SchemeSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v3.1 signers associated with the APK's
+ * signature.
+ *
+ * <note> Multiple signers represent different targeted platform versions, not
+ * a signing identity of multiple signers. APK Signature Scheme v3.1 only supports single
+ * signer identities.</note>
+ */
+ public List<V3SchemeSignerInfo> getV31SchemeSigners() {
+ return mV31SchemeSigners;
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v4 signers associated with the APK's
+ * signature.
+ */
+ public List<V4SchemeSignerInfo> getV4SchemeSigners() {
+ return mV4SchemeSigners;
+ }
+
+ /**
+ * Returns information about SourceStamp associated with the APK's signature.
+ */
+ public SourceStampInfo getSourceStampInfo() {
+ return mSourceStampInfo;
+ }
+
+ /**
+ * 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));
+ }
+
+ void addWarning(Issue msg, Object... parameters) {
+ mWarnings.add(new IssueWithParams(msg, parameters));
+ }
+
+ /**
+ * Sets whether warnings should be treated as errors.
+ */
+ void setWarningsAsErrors(boolean value) {
+ mWarningsAsErrors = value;
+ }
+
+ /**
+ * Returns errors encountered while verifying the APK's signatures.
+ */
+ public List<IssueWithParams> getErrors() {
+ if (!mWarningsAsErrors) {
+ return mErrors;
+ } else {
+ List<IssueWithParams> allErrors = new ArrayList<>();
+ allErrors.addAll(mErrors);
+ allErrors.addAll(mWarnings);
+ return allErrors;
+ }
+ }
+
+ /**
+ * Returns warnings encountered while verifying the APK's signatures.
+ */
+ public List<IssueWithParams> 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(ApkSigResult source) {
+ switch (source.signatureSchemeVersion) {
+ case VERSION_SOURCE_STAMP:
+ mSourceStampVerified = source.verified;
+ if (!source.mSigners.isEmpty()) {
+ mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
+ }
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown ApkSigResult Signing Block Scheme Id "
+ + source.signatureSchemeVersion);
+ }
+ }
+
+ private void mergeFrom(ApkSigningBlockUtils.Result source) {
+ if (source == null) {
+ return;
+ }
+ if (source.containsErrors()) {
+ mErrors.addAll(source.getErrors());
+ }
+ if (source.containsWarnings()) {
+ mWarnings.addAll(source.getWarnings());
+ }
+ switch (source.signatureSchemeVersion) {
+ case VERSION_APK_SIGNATURE_SCHEME_V2:
+ mVerifiedUsingV2Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV2SchemeSigners.add(new V2SchemeSignerInfo(signer));
+ }
+ break;
+ case VERSION_APK_SIGNATURE_SCHEME_V3:
+ mVerifiedUsingV3Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV3SchemeSigners.add(new V3SchemeSignerInfo(signer));
+ }
+ // Do not overwrite a previously set lineage from a v3.1 signing block.
+ if (mSigningCertificateLineage == null) {
+ mSigningCertificateLineage = source.signingCertificateLineage;
+ }
+ break;
+ case VERSION_APK_SIGNATURE_SCHEME_V31:
+ mVerifiedUsingV31Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV31SchemeSigners.add(new V3SchemeSignerInfo(signer));
+ }
+ mSigningCertificateLineage = source.signingCertificateLineage;
+ break;
+ case VERSION_APK_SIGNATURE_SCHEME_V4:
+ mVerifiedUsingV4Scheme = source.verified;
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) {
+ mV4SchemeSigners.add(new V4SchemeSignerInfo(signer));
+ }
+ break;
+ case VERSION_SOURCE_STAMP:
+ mSourceStampVerified = source.verified;
+ if (!source.signers.isEmpty()) {
+ mSourceStampInfo = new SourceStampInfo(source.signers.get(0));
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
+ }
+ }
+
+ /**
+ * 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 (mWarningsAsErrors && !mWarnings.isEmpty()) {
+ return true;
+ }
+ if (!mV1SchemeSigners.isEmpty()) {
+ for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
+ }
+ }
+ if (!mV2SchemeSigners.isEmpty()) {
+ for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
+ }
+ }
+ if (!mV3SchemeSigners.isEmpty()) {
+ for (V3SchemeSignerInfo signer : mV3SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
+ }
+ }
+ if (!mV31SchemeSigners.isEmpty()) {
+ for (V3SchemeSignerInfo signer : mV31SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) {
+ return true;
+ }
+ }
+ }
+ if (mSourceStampInfo != null) {
+ if (mSourceStampInfo.containsErrors()) {
+ return true;
+ }
+ if (mWarningsAsErrors && !mSourceStampInfo.getWarnings().isEmpty()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns all errors for this result, including any errors from signature scheme signers
+ * and the source stamp.
+ */
+ public List<IssueWithParams> getAllErrors() {
+ List<IssueWithParams> errors = new ArrayList<>();
+ errors.addAll(mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(mWarnings);
+ }
+ if (!mV1SchemeSigners.isEmpty()) {
+ for (V1SchemeSignerInfo signer : mV1SchemeSigners) {
+ errors.addAll(signer.mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(signer.getWarnings());
+ }
+ }
+ }
+ if (!mV2SchemeSigners.isEmpty()) {
+ for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+ errors.addAll(signer.mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(signer.getWarnings());
+ }
+ }
+ }
+ if (!mV3SchemeSigners.isEmpty()) {
+ for (V3SchemeSignerInfo signer : mV3SchemeSigners) {
+ errors.addAll(signer.mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(signer.getWarnings());
+ }
+ }
+ }
+ if (!mV31SchemeSigners.isEmpty()) {
+ for (V3SchemeSignerInfo signer : mV31SchemeSigners) {
+ errors.addAll(signer.mErrors);
+ if (mWarningsAsErrors) {
+ errors.addAll(signer.getWarnings());
+ }
+ }
+ }
+ if (mSourceStampInfo != null) {
+ errors.addAll(mSourceStampInfo.getErrors());
+ if (mWarningsAsErrors) {
+ errors.addAll(mSourceStampInfo.getWarnings());
+ }
+ }
+ return errors;
+ }
+
+ /**
+ * Information about a JAR signer associated with the APK's signature.
+ */
+ public static class V1SchemeSignerInfo {
+ private final String mName;
+ private final List<X509Certificate> mCertChain;
+ private final String mSignatureBlockFileName;
+ private final String mSignatureFileName;
+
+ private final List<IssueWithParams> mErrors;
+ private final List<IssueWithParams> 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()}.
+ *
+ * <p>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<X509Certificate> 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<IssueWithParams> 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<IssueWithParams> 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<X509Certificate> mCerts;
+
+ private final List<IssueWithParams> mErrors;
+ private final List<IssueWithParams> mWarnings;
+ private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+ mContentDigests;
+
+ private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ mIndex = result.index;
+ mCerts = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ mContentDigests = result.contentDigests;
+ }
+
+ /**
+ * 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()}.
+ *
+ * <p>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<X509Certificate> getCertificates() {
+ return mCerts;
+ }
+
+ private void addError(Issue msg, Object... parameters) {
+ mErrors.add(new IssueWithParams(msg, parameters));
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+
+ public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+ return mContentDigests;
+ }
+ }
+
+ /**
+ * 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<X509Certificate> mCerts;
+
+ private final List<IssueWithParams> mErrors;
+ private final List<IssueWithParams> mWarnings;
+ private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+ mContentDigests;
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
+ private final SigningCertificateLineage mSigningCertificateLineage;
+
+ private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ mIndex = result.index;
+ mCerts = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ mContentDigests = result.contentDigests;
+ mMinSdkVersion = result.minSdkVersion;
+ mMaxSdkVersion = result.maxSdkVersion;
+ mSigningCertificateLineage = result.signingCertificateLineage;
+ mRotationTargetsDevRelease = result.additionalAttributes.stream().mapToInt(
+ attribute -> attribute.getId()).anyMatch(
+ attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
+ }
+
+ /**
+ * 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()}.
+ *
+ * <p>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<X509Certificate> getCertificates() {
+ return mCerts;
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+
+ public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+ return mContentDigests;
+ }
+
+ /**
+ * Returns the minimum SDK version on which this signer should be verified.
+ */
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ /**
+ * Returns the maximum SDK version on which this signer should be verified.
+ */
+ public int getMaxSdkVersion() {
+ return mMaxSdkVersion;
+ }
+
+ /**
+ * Returns whether rotation is targeting a development release.
+ *
+ * <p>A development release uses the SDK version of the previously released platform
+ * until the SDK of the development release is finalized. To allow rotation to target
+ * a development release after T, this attribute must be set to ensure rotation is
+ * used on the development release but ignored on the released platform with the same
+ * API level.
+ */
+ public boolean getRotationTargetsDevRelease() {
+ return mRotationTargetsDevRelease;
+ }
+
+ /**
+ * Returns the {@link SigningCertificateLineage} for this signer; when an APK has
+ * SDK targeted signing configs, the lineage of each signer could potentially contain
+ * a subset of the full signing lineage and / or different capabilities for each signer
+ * in the lineage.
+ */
+ public SigningCertificateLineage getSigningCertificateLineage() {
+ return mSigningCertificateLineage;
+ }
+ }
+
+ /**
+ * Information about an APK Signature Scheme V4 signer associated with the APK's
+ * signature.
+ */
+ public static class V4SchemeSignerInfo {
+ private final int mIndex;
+ private final List<X509Certificate> mCerts;
+
+ private final List<IssueWithParams> mErrors;
+ private final List<IssueWithParams> mWarnings;
+ private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest>
+ mContentDigests;
+
+ private V4SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) {
+ mIndex = result.index;
+ mCerts = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ mContentDigests = result.contentDigests;
+ }
+
+ /**
+ * 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()}.
+ *
+ * <p>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<X509Certificate> getCertificates() {
+ return mCerts;
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+
+ public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() {
+ return mContentDigests;
+ }
+ }
+
+ /**
+ * Information about SourceStamp associated with the APK's signature.
+ */
+ public static class SourceStampInfo {
+ public enum SourceStampVerificationStatus {
+ /** The stamp is present and was successfully verified. */
+ STAMP_VERIFIED,
+ /** The stamp is present but failed verification. */
+ STAMP_VERIFICATION_FAILED,
+ /** The expected cert digest did not match the digest in the APK. */
+ CERT_DIGEST_MISMATCH,
+ /** The stamp is not present at all. */
+ STAMP_MISSING,
+ /** The stamp is at least partially present, but was not able to be verified. */
+ STAMP_NOT_VERIFIED,
+ /** The stamp was not able to be verified due to an unexpected error. */
+ VERIFICATION_ERROR
+ }
+
+ private final List<X509Certificate> mCertificates;
+ private final List<X509Certificate> mCertificateLineage;
+
+ private final List<IssueWithParams> mErrors;
+ private final List<IssueWithParams> mWarnings;
+ private final List<IssueWithParams> mInfoMessages;
+
+ private final SourceStampVerificationStatus mSourceStampVerificationStatus;
+
+ private final long mTimestamp;
+
+ private SourceStampInfo(ApkSignerInfo result) {
+ mCertificates = result.certs;
+ mCertificateLineage = result.certificateLineage;
+ mErrors = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+ result.getErrors());
+ mWarnings = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+ result.getWarnings());
+ mInfoMessages = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues(
+ result.getInfoMessages());
+ if (mErrors.isEmpty() && mWarnings.isEmpty()) {
+ mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED;
+ } else {
+ mSourceStampVerificationStatus =
+ SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED;
+ }
+ mTimestamp = result.timestamp;
+ }
+
+ SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) {
+ mCertificates = Collections.emptyList();
+ mCertificateLineage = Collections.emptyList();
+ mErrors = Collections.emptyList();
+ mWarnings = Collections.emptyList();
+ mInfoMessages = Collections.emptyList();
+ mSourceStampVerificationStatus = sourceStampVerificationStatus;
+ mTimestamp = 0;
+ }
+
+ /**
+ * Returns the SourceStamp's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * <p>This certificate contains the SourceStamp's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCertificates.isEmpty() ? null : mCertificates.get(0);
+ }
+
+ /**
+ * Returns a list containing all of the certificates in the stamp certificate lineage.
+ */
+ public List<X509Certificate> getCertificatesInLineage() {
+ return mCertificateLineage;
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ /**
+ * Returns {@code true} if any info messages were encountered during verification of
+ * this source stamp.
+ */
+ public boolean containsInfoMessages() {
+ return !mInfoMessages.isEmpty();
+ }
+
+ public List<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+
+ /**
+ * Returns a {@code List} of {@link IssueWithParams} representing info messages
+ * that were encountered during verification of the source stamp.
+ */
+ public List<IssueWithParams> getInfoMessages() {
+ return mInfoMessages;
+ }
+
+ /**
+ * Returns the reason for any source stamp verification failures, or {@code
+ * STAMP_VERIFIED} if the source stamp was successfully verified.
+ */
+ public SourceStampVerificationStatus getSourceStampVerificationStatus() {
+ return mSourceStampVerificationStatus;
+ }
+
+ /**
+ * Returns the epoch timestamp in seconds representing the time this source stamp block
+ * was signed, or 0 if the timestamp is not available.
+ */
+ public long getTimestampEpochSeconds() {
+ return mTimestamp;
+ }
+ }
+ }
+
+ /**
+ * Error or warning encountered while verifying an APK's signatures.
+ */
+ public enum Issue {
+
+ /**
+ * APK is not JAR-signed.
+ */
+ JAR_SIG_NO_SIGNATURES("No JAR signatures"),
+
+ /**
+ * APK signature scheme v1 has exceeded the maximum number of jar signers.
+ * <ul>
+ * <li>Parameter 1: maximum allowed signers ({@code Integer})</li>
+ * <li>Parameter 2: total number of signers ({@code Integer})</li>
+ * </ul>
+ */
+ JAR_SIG_MAX_SIGNATURES_EXCEEDED(
+ "APK Signature Scheme v1 only supports a maximum of %1$d signers, found %2$d"),
+
+ /**
+ * 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.
+ *
+ * <ul>
+ * <li>Parameter 1: name ({@code String})</li>
+ * </ul>
+ */
+ JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"),
+
+ /**
+ * JAR manifest contains a section with a duplicate name.
+ *
+ * <ul>
+ * <li>Parameter 1: section name ({@code String})</li>
+ * </ul>
+ */
+ JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"),
+
+ /**
+ * JAR manifest contains a section without a name.
+ *
+ * <ul>
+ * <li>Parameter 1: section index (1-based) ({@code Integer})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: signature file name ({@code String})</li>
+ * <li>Parameter 2: section index (1-based) ({@code Integer})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: entry name ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: entry name ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: entry name ({@code String})</li>
+ * <li>Parameter 2: signature file name ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: entry name ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: first entry name ({@code String})</li>
+ * <li>Parameter 2: first entry signer names ({@code List<String>})</li>
+ * <li>Parameter 3: second entry name ({@code String})</li>
+ * <li>Parameter 4: second entry signer names ({@code List<String>})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: entry name ({@code String})</li>
+ * <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li>
+ * <li>Parameter 3: name of the entry in which the expected digest is specified
+ * ({@code String})</li>
+ * <li>Parameter 4: base64-encoded actual digest ({@code String})</li>
+ * <li>Parameter 5: base64-encoded expected digest ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: digest algorithm (e.g., SHA-256) ({@code String})</li>
+ * <li>Parameter 2: name of the entry in which the expected digest is specified
+ * ({@code String})</li>
+ * <li>Parameter 3: base64-encoded actual digest ({@code String})</li>
+ * <li>Parameter 4: base64-encoded expected digest ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: section name ({@code String})</li>
+ * <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li>
+ * <li>Parameter 3: name of the signature file in which the expected digest is specified
+ * ({@code String})</li>
+ * <li>Parameter 4: base64-encoded actual digest ({@code String})</li>
+ * <li>Parameter 5: base64-encoded expected digest ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the signature file ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the signature file ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the encountered file ({@code String})</li>
+ * <li>Parameter 2: name of the missing file ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the signature block file ({@code String})</li>
+ * <li>Parameter 2: name of the signature file ({@code String})</li>
+ * <li>Parameter 3: exception ({@code Throwable})</li>
+ * </ul>
+ */
+ JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"),
+
+ /**
+ * JAR signature contains unsupported digest algorithm.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the signature block file ({@code String})</li>
+ * <li>Parameter 2: digest algorithm OID ({@code String})</li>
+ * <li>Parameter 3: signature algorithm OID ({@code String})</li>
+ * <li>Parameter 4: API Levels on which this combination of algorithms is not supported
+ * ({@code String})</li>
+ * <li>Parameter 5: user-friendly variant of digest algorithm ({@code String})</li>
+ * <li>Parameter 6: user-friendly variant of signature algorithm ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the signature block file ({@code String})</li>
+ * <li>Parameter 2: exception ({@code Throwable})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the signature block file ({@code String})</li>
+ * <li>Parameter 2: exception ({@code Throwable})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the signature block file ({@code String})</li>
+ * <li>Parameter 2: name of the signature file ({@code String})</li>
+ * </ul>
+ */
+ JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"),
+
+ /**
+ * JAR signature contains no verified signers.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the signature block file ({@code String})</li>
+ * </ul>
+ */
+ JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"),
+
+ /**
+ * JAR signature file contains a section with a duplicate name.
+ *
+ * <ul>
+ * <li>Parameter 1: signature file name ({@code String})</li>
+ * <li>Parameter 1: section name ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: signature file name ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the signature file ({@code String})</li>
+ * <li>Parameter 2: unknown APK signature scheme ID ({@code} Integer)</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: name of the signature file ({@code String})</li>
+ * <li>Parameter 2: APK signature scheme ID ({@code} Integer)</li>
+ * <li>Parameter 3: APK signature scheme English name ({@code} String)</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: entry name ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: target sandbox version ({@code Integer})</li>
+ * </ul>
+ */
+ NO_SIG_FOR_TARGET_SANDBOX_VERSION(
+ "Missing APK Signature Scheme v2 signature required for target sandbox version"
+ + " %1$d"),
+
+ /**
+ * APK is targeting an SDK version that requires a minimum signature scheme version, but the
+ * APK is not signed with that version or later.
+ *
+ * <ul>
+ * <li>Parameter 1: target SDK Version (@code Integer})</li>
+ * <li>Parameter 2: minimum signature scheme version ((@code Integer})</li>
+ * </ul>
+ */
+ MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET(
+ "Target SDK version %1$d requires a minimum of signature scheme v%2$d; the APK is"
+ + " not signed with this or a later signature scheme"),
+
+ /**
+ * 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.
+ *
+ * <ul>
+ * <li>Parameter 1: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+ /**
+ * This APK Signature Scheme v2 signer's certificate could not be parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+ * certificates ({@code Integer})</li>
+ * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+ * list of certificates ({@code Integer})</li>
+ * <li>Parameter 3: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+ * </ul>
+ */
+ V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains a malformed additional attribute.
+ *
+ * <ul>
+ * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
+ * </ul>
+ */
+ V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
+
+ /**
+ * APK Signature Scheme v2 signature references an unknown APK signature scheme ID.
+ *
+ * <ul>
+ * <li>Parameter 1: signer index ({@code Integer})</li>
+ * <li>Parameter 2: unknown APK signature scheme ID ({@code} Integer)</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: signer index ({@code Integer})</li>
+ * <li>Parameter 2: APK signature scheme English name ({@code} String)</li>
+ * </ul>
+ */
+ 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 has exceeded the maximum number of signers.
+ * <ul>
+ * <li>Parameter 1: maximum allowed signers ({@code Integer})</li>
+ * <li>Parameter 2: total number of signers ({@code Integer})</li>
+ * </ul>
+ */
+ V2_SIG_MAX_SIGNATURES_EXCEEDED(
+ "APK Signature Scheme V2 only supports a maximum of %1$d signers, found %2$d"),
+
+ /**
+ * 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.
+ *
+ * <ul>
+ * <li>Parameter 1: algorithm ID ({@code Integer})</li>
+ * </ul>
+ */
+ V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains an unknown additional attribute.
+ *
+ * <ul>
+ * <li>Parameter 1: attribute ID ({@code Integer})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+ * <li>Parameter 2: exception ({@code Throwable})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+ * </ul>
+ */
+ 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: %1$s"),
+
+ /**
+ * 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.
+ *
+ * <ul>
+ * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
+ * <li>Parameter 2: hex-encoded public key from signatures record ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithms from signatures record ({@code List<Integer>})</li>
+ * <li>Parameter 2: signature algorithms from digests record ({@code List<Integer>})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+ * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
+ * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ V3_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+ /**
+ * This APK Signature Scheme v3 signer's certificate could not be parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+ * certificates ({@code Integer})</li>
+ * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+ * list of certificates ({@code Integer})</li>
+ * <li>Parameter 3: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+ * </ul>
+ */
+ V3_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v3 digest record #%1$d"),
+
+ /**
+ * This APK Signature Scheme v3 signer contains a malformed additional attribute.
+ *
+ * <ul>
+ * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: algorithm ID ({@code Integer})</li>
+ * </ul>
+ */
+ V3_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+ /**
+ * This APK Signature Scheme v3 signer contains an unknown additional attribute.
+ *
+ * <ul>
+ * <li>Parameter 1: attribute ID ({@code Integer})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+ * <li>Parameter 2: exception ({@code Throwable})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: minSdkVersion ({@code Integer})
+ * <li>Parameter 2: maxSdkVersion ({@code Integer})
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: minSdkVersion in signature record ({@code Integer}) </li>
+ * <li>Parameter 2: minSdkVersion in signed data ({@code Integer}) </li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: maxSdkVersion in signature record ({@code Integer}) </li>
+ * <li>Parameter 2: maxSdkVersion in signed data ({@code Integer}) </li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
+ * <li>Parameter 2: hex-encoded public key from signatures record ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithms from signatures record ({@code List<Integer>})</li>
+ * <li>Parameter 2: signature algorithms from digests record ({@code List<Integer>})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+ * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
+ * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
+ * </ul>
+ */
+ 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.
+ *
+ * <ul>
+ * <li>Parameter 1: minSdkVersion ({@code Integer})
+ * <li>Parameter 2: maxSdkVersion ({@code Integer})
+ * </ul>
+ */
+ 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."),
+
+ /**
+ * The v3 stripping protection attribute for rotation is present, but a v3.1 signing block
+ * was not found.
+ *
+ * <ul>
+ * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+ * </ul>
+ */
+ V31_BLOCK_MISSING(
+ "The v3 signer indicates key rotation should be supported starting from SDK "
+ + "version %1$s, but a v3.1 block was not found"),
+
+ /**
+ * The v3 stripping protection attribute for rotation does not match the minimum SDK version
+ * targeting rotation in the v3.1 signer block.
+ *
+ * <ul>
+ * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer})
+ * <li>Parameter 2: min SDK version supporting rotation from v3.1 block ({@code Integer})
+ * </ul>
+ */
+ V31_ROTATION_MIN_SDK_MISMATCH(
+ "The v3 signer indicates key rotation should be supported starting from SDK "
+ + "version %1$s, but the v3.1 block targets %2$s for rotation"),
+
+ /**
+ * The APK supports key rotation with SDK version targeting using v3.1, but the rotation min
+ * SDK version stripping protection attribute was not written to the v3 signer.
+ *
+ * <ul>
+ * <li>Parameter 1: min SDK version supporting rotation from v3.1 block ({@code Integer})
+ * </ul>
+ */
+ V31_ROTATION_MIN_SDK_ATTR_MISSING(
+ "APK supports key rotation starting from SDK version %1$s, but the v3 signer does"
+ + " not contain the attribute to detect if this signature is stripped"),
+
+ /**
+ * The APK contains a v3.1 signing block without a v3.0 block. The v3.1 block should only
+ * be used for targeting rotation for a later SDK version; if an APK's minSdkVersion is the
+ * same as the SDK version for rotation then this should be written to a v3.0 block.
+ */
+ V31_BLOCK_FOUND_WITHOUT_V3_BLOCK(
+ "The APK contains a v3.1 signing block without a v3.0 base block"),
+
+ /**
+ * The APK contains a v3.0 signing block with a rotation-targets-dev-release attribute in
+ * the signer; this attribute is only intended for v3.1 signers to indicate they should be
+ * targeting the next development release that is using the SDK version of the previously
+ * released platform SDK version.
+ */
+ V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER(
+ "The rotation-targets-dev-release attribute is only supported on v3.1 signers; "
+ + "this attribute will be ignored by the platform in a v3.0 signer"),
+
+ /**
+ * APK Signing Block contains an unknown entry.
+ *
+ * <ul>
+ * <li>Parameter 1: entry ID ({@code Integer})</li>
+ * </ul>
+ */
+ APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"),
+
+ /**
+ * Failed to parse this signer's signature record contained in the APK Signature Scheme
+ * V4 signature.
+ *
+ * <ul>
+ * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+ * </ul>
+ */
+ V4_SIG_MALFORMED_SIGNERS(
+ "V4 signature has malformed signer block"),
+
+ /**
+ * This APK Signature Scheme V4 signer contains a signature produced using an
+ * unknown algorithm.
+ *
+ * <ul>
+ * <li>Parameter 1: algorithm ID ({@code Integer})</li>
+ * </ul>
+ */
+ V4_SIG_UNKNOWN_SIG_ALGORITHM(
+ "V4 signature has unknown signing algorithm: %1$#x"),
+
+ /**
+ * This APK Signature Scheme V4 signer offers no signatures.
+ */
+ V4_SIG_NO_SIGNATURES(
+ "V4 signature has no signature found"),
+
+ /**
+ * This APK Signature Scheme V4 signer offers signatures but none of them are
+ * supported.
+ */
+ V4_SIG_NO_SUPPORTED_SIGNATURES(
+ "V4 signature has no supported signature"),
+
+ /**
+ * APK Signature Scheme v3 signature over this signer's signed-data block did not verify.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+ * </ul>
+ */
+ V4_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /**
+ * An exception was encountered while verifying APK Signature Scheme v3 signature of this
+ * signer.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+ * <li>Parameter 2: exception ({@code Throwable})</li>
+ * </ul>
+ */
+ V4_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * Public key embedded in the APK Signature Scheme v4 signature of this signer could not be
+ * parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ V4_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+ /**
+ * This APK Signature Scheme V4 signer's certificate could not be parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+ * certificates ({@code Integer})</li>
+ * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+ * list of certificates ({@code Integer})</li>
+ * <li>Parameter 3: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ V4_SIG_MALFORMED_CERTIFICATE(
+ "V4 signature has malformed certificate"),
+
+ /**
+ * This APK Signature Scheme V4 signer offers no certificate.
+ */
+ V4_SIG_NO_CERTIFICATE("V4 signature has no certificate"),
+
+ /**
+ * This APK Signature Scheme V4 signer's public key listed in the signer's
+ * certificate does not match the public key listed in the signature proto.
+ *
+ * <ul>
+ * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
+ * <li>Parameter 2: hex-encoded public key from signature proto ({@code String})</li>
+ * </ul>
+ */
+ V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+ "V4 signature has mismatched certificate and signature: <%1$s> vs <%2$s>"),
+
+ /**
+ * The APK's hash root (aka digest) does not match the hash root contained in the Signature
+ * Scheme V4 signature.
+ *
+ * <ul>
+ * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+ * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
+ * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
+ * </ul>
+ */
+ V4_SIG_APK_ROOT_DID_NOT_VERIFY(
+ "V4 signature's hash tree root (content digest) did not verity"),
+
+ /**
+ * The APK's hash tree does not match the hash tree contained in the Signature
+ * Scheme V4 signature.
+ *
+ * <ul>
+ * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+ * <li>Parameter 2: hex-encoded expected hash tree of the APK ({@code String})</li>
+ * <li>Parameter 3: hex-encoded actual hash tree of the APK ({@code String})</li>
+ * </ul>
+ */
+ V4_SIG_APK_TREE_DID_NOT_VERIFY(
+ "V4 signature's hash tree did not verity"),
+
+ /**
+ * Using more than one Signer to sign APK Signature Scheme V4 signature.
+ */
+ V4_SIG_MULTIPLE_SIGNERS(
+ "V4 signature only supports one signer"),
+
+ /**
+ * V4.1 signature requires two signers to match the v3 and the v3.1.
+ */
+ V41_SIG_NEEDS_TWO_SIGNERS("V4.1 signature requires two signers"),
+
+ /**
+ * The signer used to sign APK Signature Scheme V2/V3 signature does not match the signer
+ * used to sign APK Signature Scheme V4 signature.
+ */
+ V4_SIG_V2_V3_SIGNERS_MISMATCH(
+ "V4 signature and V2/V3 signature have mismatched certificates"),
+
+ /**
+ * The v4 signature's digest does not match the digest from the corresponding v2 / v3
+ * signature.
+ *
+ * <ul>
+ * <li>Parameter 1: Signature scheme of mismatched digest ({@code int})
+ * <li>Parameter 2: v2/v3 digest ({@code String})
+ * <li>Parameter 3: v4 digest ({@code String})
+ * </ul>
+ */
+ V4_SIG_V2_V3_DIGESTS_MISMATCH(
+ "V4 signature and V%1$d signature have mismatched digests, V%1$d digest: %2$s, V4"
+ + " digest: %3$s"),
+
+ /**
+ * The v4 signature does not contain the expected number of digests.
+ *
+ * <ul>
+ * <li>Parameter 1: Number of digests found ({@code int})
+ * </ul>
+ */
+ V4_SIG_UNEXPECTED_DIGESTS(
+ "V4 signature does not have the expected number of digests, found %1$d"),
+
+ /**
+ * The v4 signature format version isn't the same as the tool's current version, something
+ * may go wrong.
+ */
+ V4_SIG_VERSION_NOT_CURRENT(
+ "V4 signature format version %1$d is different from the tool's current "
+ + "version %2$d"),
+
+ /**
+ * The APK does not contain the source stamp certificate digest file nor the signature block
+ * when verification expected a source stamp to be present.
+ */
+ SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING(
+ "Neither the source stamp certificate digest file nor the signature block are "
+ + "present in the APK"),
+
+ /** APK contains SourceStamp file, but does not contain a SourceStamp signature. */
+ SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"),
+
+ /**
+ * SourceStamp's certificate could not be parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: error details ({@code Throwable})
+ * </ul>
+ */
+ SOURCE_STAMP_MALFORMED_CERTIFICATE("Malformed certificate: %1$s"),
+
+ /** Failed to parse SourceStamp's signature. */
+ SOURCE_STAMP_MALFORMED_SIGNATURE("Malformed SourceStamp signature"),
+
+ /**
+ * SourceStamp contains a signature produced using an unknown algorithm.
+ *
+ * <ul>
+ * <li>Parameter 1: algorithm ID ({@code Integer})
+ * </ul>
+ */
+ SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+ /**
+ * An exception was encountered while verifying SourceStamp signature.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ * <li>Parameter 2: exception ({@code Throwable})
+ * </ul>
+ */
+ SOURCE_STAMP_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * SourceStamp signature block did not verify.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})
+ * </ul>
+ */
+ SOURCE_STAMP_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /** SourceStamp offers no signatures. */
+ SOURCE_STAMP_NO_SIGNATURE("No signature"),
+
+ /**
+ * SourceStamp offers an unsupported signature.
+ * <ul>
+ * <li>Parameter 1: list of {@link SignatureAlgorithm}s in the source stamp
+ * signing block.
+ * <li>Parameter 2: {@code Exception} caught when attempting to obtain the list of
+ * supported signatures.
+ * </ul>
+ */
+ SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature(s) {%1$s} not supported: %2$s"),
+
+ /**
+ * SourceStamp's certificate listed in the APK signing block does not match the certificate
+ * listed in the SourceStamp file in the APK.
+ *
+ * <ul>
+ * <li>Parameter 1: SHA-256 hash of certificate from SourceStamp block in APK signing
+ * block ({@code String})
+ * <li>Parameter 2: SHA-256 hash of certificate from SourceStamp file in APK ({@code
+ * String})
+ * </ul>
+ */
+ SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK(
+ "Certificate mismatch between SourceStamp block in APK signing block and"
+ + " SourceStamp file in APK: <%1$s> vs <%2$s>"),
+
+ /**
+ * The APK contains a source stamp signature block without the expected certificate digest
+ * in the APK contents.
+ */
+ SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST(
+ "A source stamp signature block was found without a corresponding certificate "
+ + "digest in the APK"),
+
+ /**
+ * When verifying just the source stamp, the certificate digest in the APK does not match
+ * the expected digest.
+ * <ul>
+ * <li>Parameter 1: SHA-256 digest of the source stamp certificate in the APK.
+ * <li>Parameter 2: SHA-256 digest of the expected source stamp certificate.
+ * </ul>
+ */
+ SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH(
+ "The source stamp certificate digest in the APK, %1$s, does not match the "
+ + "expected digest, %2$s"),
+
+ /**
+ * Source stamp block contains a malformed attribute.
+ *
+ * <ul>
+ * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
+ * </ul>
+ */
+ SOURCE_STAMP_MALFORMED_ATTRIBUTE("Malformed stamp attribute #%1$d"),
+
+ /**
+ * Source stamp block contains an unknown attribute.
+ *
+ * <ul>
+ * <li>Parameter 1: attribute ID ({@code Integer})</li>
+ * </ul>
+ */
+ SOURCE_STAMP_UNKNOWN_ATTRIBUTE("Unknown stamp attribute: ID %1$#x"),
+
+ /**
+ * Failed to parse the SigningCertificateLineage structure in the source stamp
+ * attributes section.
+ */
+ SOURCE_STAMP_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage "
+ + "structure in the source stamp attributes section."),
+
+ /**
+ * The source stamp certificate does not match the terminal node in the provided
+ * proof-of-rotation structure describing the stamp certificate history.
+ */
+ SOURCE_STAMP_POR_CERT_MISMATCH(
+ "APK signing certificate differs from the associated certificate found in the "
+ + "signer's SigningCertificateLineage."),
+
+ /**
+ * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
+ * with signature(s) that did not verify.
+ */
+ SOURCE_STAMP_POR_DID_NOT_VERIFY("Source stamp SigningCertificateLineage attribute "
+ + "contains a proof-of-rotation record with signature(s) that did not verify."),
+
+ /**
+ * The source stamp timestamp attribute has an invalid value (<= 0).
+ * <ul>
+ * <li>Parameter 1: The invalid timestamp value.
+ * </ul>
+ */
+ SOURCE_STAMP_INVALID_TIMESTAMP(
+ "The source stamp"
+ + " timestamp attribute has an invalid value: %1$d"),
+
+ /**
+ * The APK could not be properly parsed due to a ZIP or APK format exception.
+ * <ul>
+ * <li>Parameter 1: The {@code Exception} caught when attempting to parse the APK.
+ * </ul>
+ */
+ MALFORMED_APK(
+ "Malformed APK; the following exception was caught when attempting to parse the "
+ + "APK: %1$s"),
+
+ /**
+ * An unexpected exception was caught when attempting to verify the signature(s) within the
+ * APK.
+ * <ul>
+ * <li>Parameter 1: The {@code Exception} caught during verification.
+ * </ul>
+ */
+ UNEXPECTED_EXCEPTION(
+ "An unexpected exception was caught when verifying the signature: %1$s");
+
+ private final String mFormat;
+
+ 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 extends ApkVerificationIssue {
+ 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) {
+ super(issue.mFormat, 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 instanceof ByteArray)) {
+ 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.
+ *
+ * <p>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 File mV4SignatureFile;
+
+ 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)}.
+ *
+ * <p>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;
+ }
+
+ public Builder setV4SignatureFile(File v4SignatureFile) {
+ mV4SignatureFile = v4SignatureFile;
+ return this;
+ }
+
+ /**
+ * Returns an {@link ApkVerifier} initialized according to the configuration of this
+ * builder.
+ */
+ public ApkVerifier build() {
+ return new ApkVerifier(
+ mApkFile,
+ mApkDataSource,
+ mV4SignatureFile,
+ mMinSdkVersion,
+ mMaxSdkVersion);
+ }
+ }
+
+ /**
+ * Adapter for converting base {@link ApkVerificationIssue} instances to their {@link
+ * IssueWithParams} equivalent.
+ */
+ public static class ApkVerificationIssueAdapter {
+ private ApkVerificationIssueAdapter() {
+ }
+
+ // This field is visible for testing
+ static final Map<Integer, Issue> sVerificationIssueIdToIssue = new HashMap<>();
+
+ static {
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS,
+ Issue.V2_SIG_MALFORMED_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNERS,
+ Issue.V2_SIG_NO_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER,
+ Issue.V2_SIG_MALFORMED_SIGNER);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNATURE,
+ Issue.V2_SIG_MALFORMED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNATURES,
+ Issue.V2_SIG_NO_SIGNATURES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE,
+ Issue.V2_SIG_MALFORMED_CERTIFICATE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_CERTIFICATES,
+ Issue.V2_SIG_NO_CERTIFICATES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST,
+ Issue.V2_SIG_MALFORMED_DIGEST);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS,
+ Issue.V3_SIG_MALFORMED_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNERS,
+ Issue.V3_SIG_NO_SIGNERS);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER,
+ Issue.V3_SIG_MALFORMED_SIGNER);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNATURE,
+ Issue.V3_SIG_MALFORMED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNATURES,
+ Issue.V3_SIG_NO_SIGNATURES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE,
+ Issue.V3_SIG_MALFORMED_CERTIFICATE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_CERTIFICATES,
+ Issue.V3_SIG_NO_CERTIFICATES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST,
+ Issue.V3_SIG_MALFORMED_DIGEST);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE,
+ Issue.SOURCE_STAMP_NO_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE,
+ Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
+ Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE,
+ Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY,
+ Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION,
+ Issue.SOURCE_STAMP_VERIFY_EXCEPTION);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
+ Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST,
+ Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING,
+ Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
+ Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+ sVerificationIssueIdToIssue.put(
+ ApkVerificationIssue
+ .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+ Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.MALFORMED_APK,
+ Issue.MALFORMED_APK);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.UNEXPECTED_EXCEPTION,
+ Issue.UNEXPECTED_EXCEPTION);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING,
+ Issue.SOURCE_STAMP_SIG_MISSING);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
+ Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE,
+ Issue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE,
+ Issue.SOURCE_STAMP_MALFORMED_LINEAGE);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH,
+ Issue.SOURCE_STAMP_POR_CERT_MISMATCH);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY,
+ Issue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES,
+ Issue.JAR_SIG_NO_SIGNATURES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+ Issue.JAR_SIG_PARSE_EXCEPTION);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
+ Issue.SOURCE_STAMP_INVALID_TIMESTAMP);
+ }
+
+ /**
+ * Converts the provided {@code verificationIssues} to a {@code List} of corresponding
+ * {@link IssueWithParams} instances.
+ */
+ public static List<IssueWithParams> getIssuesFromVerificationIssues(
+ List<? extends ApkVerificationIssue> verificationIssues) {
+ List<IssueWithParams> result = new ArrayList<>(verificationIssues.size());
+ for (ApkVerificationIssue issue : verificationIssues) {
+ if (issue instanceof IssueWithParams) {
+ result.add((IssueWithParams) issue);
+ } else {
+ result.add(
+ new IssueWithParams(sVerificationIssueIdToIssue.get(issue.getIssueId()),
+ issue.getParams()));
+ }
+ }
+ return result;
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/Constants.java b/platform/android/java/editor/src/main/java/com/android/apksig/Constants.java
new file mode 100644
index 0000000000..dd33028cd5
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/Constants.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.v1.V1SchemeConstants;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+
+/**
+ * Exports internally defined constants to allow clients to reference these values without relying
+ * on internal code.
+ */
+public class Constants {
+ private Constants() {}
+
+ public static final int VERSION_SOURCE_STAMP = 0;
+ public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
+
+ /**
+ * The maximum number of signers supported by the v1 and v2 APK Signature Schemes.
+ */
+ public static final int MAX_APK_SIGNERS = 10;
+
+ /**
+ * The default page alignment for native library files in bytes.
+ */
+ public static final short LIBRARY_PAGE_ALIGNMENT_BYTES = 16384;
+
+ public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+ public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+
+ public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID =
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+ public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
+
+ public static final int V1_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
+ public static final int V2_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
+
+ public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/platform/android/java/editor/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
new file mode 100644
index 0000000000..957f48ad43
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -0,0 +1,2241 @@
+/*
+ * 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 static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERITY_PADDING_BLOCK_ID;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT;
+
+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.stamp.V2SourceStampSigner;
+import com.android.apksig.internal.apk.v1.DigestAlgorithm;
+import com.android.apksig.internal.apk.v1.V1SchemeConstants;
+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.V3SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v4.V4SchemeSigner;
+import com.android.apksig.internal.apk.v4.V4Signature;
+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.File;
+import java.io.IOException;
+import java.io.OutputStream;
+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.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Default implementation of {@link ApkSignerEngine}.
+ *
+ * <p>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 mVerityEnabled;
+ private final boolean mDebuggableApkPermitted;
+ private final boolean mOtherSignersSignaturesPreserved;
+ private final String mCreatedBy;
+ private final List<SignerConfig> mSignerConfigs;
+ private final List<SignerConfig> mTargetedSignerConfigs;
+ private final SignerConfig mSourceStampSignerConfig;
+ private final SigningCertificateLineage mSourceStampSigningCertificateLineage;
+ private final boolean mSourceStampTimestampEnabled;
+ private final int mMinSdkVersion;
+ private final SigningCertificateLineage mSigningCertificateLineage;
+
+ private List<byte[]> mPreservedV2Signers = Collections.emptyList();
+ private List<Pair<byte[], Integer>> mPreservedSignatureBlocks = Collections.emptyList();
+
+ private List<V1SchemeSigner.SignerConfig> 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<String> mSignatureExpectedOutputJarEntryNames = Collections.emptySet();
+
+ /** Requests for digests of output JAR entries. */
+ private final Map<String, GetJarEntryDataDigestRequest> mOutputJarEntryDigestRequests =
+ new HashMap<>();
+
+ /** Digests of output JAR entries. */
+ private final Map<String, byte[]> mOutputJarEntryDigests = new HashMap<>();
+
+ /** Data of JAR entries emitted by this engine as v1 signature. */
+ private final Map<String, byte[]> mEmittedSignatureJarEntryData = new HashMap<>();
+
+ /** Requests for data of output JAR entries which comprise the v1 signature. */
+ private final Map<String, GetJarEntryDataRequest> 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.MULTI_THREADED;
+
+ /**
+ * A Set of block IDs to be discarded when requesting to preserve the original signatures.
+ */
+ private static final Set<Integer> DISCARDED_SIGNATURE_BLOCK_IDS;
+ static {
+ DISCARDED_SIGNATURE_BLOCK_IDS = new HashSet<>(3);
+ // The verity padding block is recomputed on an
+ // ApkSigningBlockUtils.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES boundary.
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(VERITY_PADDING_BLOCK_ID);
+ // The source stamp block is not currently preserved; appending a new signature scheme
+ // block will invalidate the previous source stamp.
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V1_SOURCE_STAMP_BLOCK_ID);
+ DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V2_SOURCE_STAMP_BLOCK_ID);
+ }
+
+ private DefaultApkSignerEngine(
+ List<SignerConfig> signerConfigs,
+ List<SignerConfig> targetedSignerConfigs,
+ SignerConfig sourceStampSignerConfig,
+ SigningCertificateLineage sourceStampSigningCertificateLineage,
+ boolean sourceStampTimestampEnabled,
+ int minSdkVersion,
+ boolean v1SigningEnabled,
+ boolean v2SigningEnabled,
+ boolean v3SigningEnabled,
+ boolean verityEnabled,
+ boolean debuggableApkPermitted,
+ boolean otherSignersSignaturesPreserved,
+ String createdBy,
+ SigningCertificateLineage signingCertificateLineage)
+ throws InvalidKeyException {
+ if (signerConfigs.isEmpty() && targetedSignerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+
+ mV1SigningEnabled = v1SigningEnabled;
+ mV2SigningEnabled = v2SigningEnabled;
+ mV3SigningEnabled = v3SigningEnabled;
+ mVerityEnabled = verityEnabled;
+ mV1SignaturePending = v1SigningEnabled;
+ mV2SignaturePending = v2SigningEnabled;
+ mV3SignaturePending = v3SigningEnabled;
+ mDebuggableApkPermitted = debuggableApkPermitted;
+ mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
+ mCreatedBy = createdBy;
+ mSignerConfigs = signerConfigs;
+ mTargetedSignerConfigs = targetedSignerConfigs;
+ mSourceStampSignerConfig = sourceStampSignerConfig;
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+ mSourceStampTimestampEnabled = sourceStampTimestampEnabled;
+ 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.isEmpty() ? signerConfigs.get(0)
+ : targetedSignerConfigs.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<SignerConfig> signerConfigs, int minSdkVersion)
+ throws InvalidKeyException {
+ mV1SignerConfigs = new ArrayList<>(signerConfigs.size());
+ Map<String, Integer> v1SignerNameToSignerIndex = new HashMap<>(signerConfigs.size());
+ DigestAlgorithm v1ContentDigestAlgorithm = null;
+ for (int i = 0; i < signerConfigs.size(); i++) {
+ SignerConfig signerConfig = signerConfigs.get(i);
+ List<X509Certificate> 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;
+ v1SignerConfig.deterministicDsaSigning = signerConfig.getDeterministicDsaSigning();
+ // 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<ApkSigningBlockUtils.SignerConfig> 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<ApkSigningBlockUtils.SignerConfig> signerConfig = new ArrayList<>();
+
+ SignerConfig oldestConfig = !mSignerConfigs.isEmpty() ? mSignerConfigs.get(0)
+ : mTargetedSignerConfigs.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(
+ oldestConfig,
+ apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2));
+ return signerConfig;
+ } else {
+ return createSigningBlockSignerConfigs(
+ apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ }
+ }
+
+ private List<ApkSigningBlockUtils.SignerConfig> processV3Configs(
+ List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException {
+ // If the caller only specified targeted signing configs, ensure those configs cover the
+ // full range for V3 support (or the APK's minSdkVersion if > P).
+ int minRequiredV3SdkVersion = Math.max(AndroidSdkVersion.P, mMinSdkVersion);
+ if (mSignerConfigs.isEmpty() &&
+ mTargetedSignerConfigs.get(0).getMinSdkVersion() > minRequiredV3SdkVersion) {
+ throw new IllegalArgumentException(
+ "The provided targeted signer configs do not cover the SDK range for V3 "
+ + "support; either provide the original signer or ensure a signer "
+ + "targets SDK version " + minRequiredV3SdkVersion);
+ }
+
+ List<ApkSigningBlockUtils.SignerConfig> 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 {
+ // If the previous signer was targeting a development release, then the current
+ // signer's maxSdkVersion should overlap with the previous signer's minSdkVersion
+ // to ensure the current signer applies to the production release.
+ ApkSigningBlockUtils.SignerConfig prevSigner = processedConfigs.get(
+ processedConfigs.size() - 1);
+ if (prevSigner.signerTargetsDevRelease) {
+ config.maxSdkVersion = prevSigner.minSdkVersion;
+ } else {
+ config.maxSdkVersion = currentMinSdk - 1;
+ }
+ }
+ if (config.minSdkVersion == V3SchemeConstants.DEV_RELEASE) {
+ // If the current signer is targeting the current development release, then set
+ // the signer's minSdkVersion to the last production release and the flag indicating
+ // this signer is targeting a dev release.
+ config.minSdkVersion = V3SchemeConstants.PROD_RELEASE;
+ config.signerTargetsDevRelease = true;
+ } else if (config.minSdkVersion == 0) {
+ config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(
+ config.signatureAlgorithms);
+ }
+ // Truncate the lineage to the current signer if it is not the latest signer.
+ X509Certificate signerCert = config.certificates.get(0);
+ if (config.signingCertificateLineage != null
+ && !config.signingCertificateLineage.isCertificateLatestInLineage(signerCert)) {
+ config.signingCertificateLineage = config.signingCertificateLineage.getSubLineage(
+ signerCert);
+ }
+ // we know that this config will be used, so add it to our result, order doesn't matter
+ // at this point
+ processedConfigs.add(config);
+ currentMinSdk = config.minSdkVersion;
+ if (config.signerTargetsDevRelease ? currentMinSdk < minRequiredV3SdkVersion
+ : currentMinSdk <= minRequiredV3SdkVersion) {
+ // 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 List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs(
+ boolean apkSigningBlockPaddingSupported) throws InvalidKeyException {
+ return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3));
+ }
+
+ private List<ApkSigningBlockUtils.SignerConfig> processV31SignerConfigs(
+ List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs) {
+ // The V3.1 signature scheme supports SDK targeted signing config, but this scheme should
+ // only be used when a separate signing config exists for the V3.0 block.
+ if (v3SignerConfigs.size() == 1) {
+ return null;
+ }
+
+ // When there are multiple signing configs, the signer with the minimum SDK version should
+ // be used for the V3.0 block, and all other signers should be used for the V3.1 block.
+ int signerMinSdkVersion = v3SignerConfigs.stream().mapToInt(
+ signer -> signer.minSdkVersion).min().orElse(AndroidSdkVersion.P);
+ List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>();
+ Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator = v3SignerConfigs.iterator();
+ while (v3SignerIterator.hasNext()) {
+ ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next();
+ // If the signer config's minSdkVersion supports V3.1 and is not the min signer in the
+ // list, then add it to the V3.1 signer configs and remove it from the V3.0 list. If
+ // the signer is targeting the minSdkVersion as a development release, then it should
+ // be included in V3.1 to allow the V3.0 block to target the production release of the
+ // same SDK version.
+ if (signerConfig.minSdkVersion >= MIN_SDK_WITH_V31_SUPPORT
+ && (signerConfig.minSdkVersion > signerMinSdkVersion
+ || (signerConfig.minSdkVersion >= signerMinSdkVersion
+ && signerConfig.signerTargetsDevRelease))) {
+ v31SignerConfigs.add(signerConfig);
+ v3SignerIterator.remove();
+ }
+ }
+ return v31SignerConfigs;
+ }
+
+ private V4SchemeSigner.SignerConfig createV4SignerConfig() throws InvalidKeyException {
+ List<ApkSigningBlockUtils.SignerConfig> v4Configs = createSigningBlockSignerConfigs(true,
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+ if (v4Configs.size() != 1) {
+ // V4 uses signer config to connect back to v3. Use the same filtering logic.
+ v4Configs = processV3Configs(v4Configs);
+ }
+ List<ApkSigningBlockUtils.SignerConfig> v41configs = processV31SignerConfigs(v4Configs);
+ return new V4SchemeSigner.SignerConfig(v4Configs, v41configs);
+ }
+
+ private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig()
+ throws InvalidKeyException {
+ ApkSigningBlockUtils.SignerConfig config = createSigningBlockSignerConfig(
+ mSourceStampSignerConfig,
+ /* apkSigningBlockPaddingSupported= */ false,
+ ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ if (mSourceStampSigningCertificateLineage != null) {
+ config.signingCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage(
+ config.certificates.get(0));
+ }
+ return config;
+ }
+
+ private int getMinSdkFromV3SignatureAlgorithms(List<SignatureAlgorithm> 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<ApkSigningBlockUtils.SignerConfig> createSigningBlockSignerConfigs(
+ boolean apkSigningBlockPaddingSupported, int schemeId) throws InvalidKeyException {
+ List<ApkSigningBlockUtils.SignerConfig> signerConfigs =
+ new ArrayList<>(mSignerConfigs.size() + mTargetedSignerConfigs.size());
+ for (int i = 0; i < mSignerConfigs.size(); i++) {
+ SignerConfig signerConfig = mSignerConfigs.get(i);
+ signerConfigs.add(
+ createSigningBlockSignerConfig(
+ signerConfig, apkSigningBlockPaddingSupported, schemeId));
+ }
+ if (schemeId >= VERSION_APK_SIGNATURE_SCHEME_V3) {
+ for (int i = 0; i < mTargetedSignerConfigs.size(); i++) {
+ SignerConfig signerConfig = mTargetedSignerConfigs.get(i);
+ signerConfigs.add(
+ createSigningBlockSignerConfig(
+ signerConfig, apkSigningBlockPaddingSupported, schemeId));
+ }
+ }
+ return signerConfigs;
+ }
+
+ private ApkSigningBlockUtils.SignerConfig createSigningBlockSignerConfig(
+ SignerConfig signerConfig, boolean apkSigningBlockPaddingSupported, int schemeId)
+ throws InvalidKeyException {
+ List<X509Certificate> certificates = signerConfig.getCertificates();
+ PublicKey publicKey = certificates.get(0).getPublicKey();
+
+ ApkSigningBlockUtils.SignerConfig newSignerConfig = new ApkSigningBlockUtils.SignerConfig();
+ newSignerConfig.privateKey = signerConfig.getPrivateKey();
+ newSignerConfig.certificates = certificates;
+ newSignerConfig.minSdkVersion = signerConfig.getMinSdkVersion();
+ newSignerConfig.signerTargetsDevRelease = signerConfig.getSignerTargetsDevRelease();
+ newSignerConfig.signingCertificateLineage = signerConfig.getSigningCertificateLineage();
+
+ switch (schemeId) {
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2:
+ newSignerConfig.signatureAlgorithms =
+ V2SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey,
+ mMinSdkVersion,
+ apkSigningBlockPaddingSupported && mVerityEnabled,
+ signerConfig.getDeterministicDsaSigning());
+ break;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3:
+ try {
+ newSignerConfig.signatureAlgorithms =
+ V3SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey,
+ mMinSdkVersion,
+ apkSigningBlockPaddingSupported && mVerityEnabled,
+ signerConfig.getDeterministicDsaSigning());
+ } 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;
+ case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4:
+ try {
+ newSignerConfig.signatureAlgorithms =
+ V4SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported,
+ signerConfig.getDeterministicDsaSigning());
+ } catch (InvalidKeyException e) {
+ // V4 is an optional signing schema, ok to proceed without.
+ newSignerConfig.signatureAlgorithms = null;
+ }
+ break;
+ case ApkSigningBlockUtils.VERSION_SOURCE_STAMP:
+ newSignerConfig.signatureAlgorithms =
+ Collections.singletonList(
+ SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
+ 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.
+ *
+ * <p>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<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
+ V1SchemeVerifier.Result result = new V1SchemeVerifier.Result();
+ Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections =
+ V1SchemeVerifier.parseManifest(manifestBytes, entryNames, result);
+ String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm);
+ for (Map.Entry<String, ManifestParser.Section> entry : sections.getSecond().entrySet()) {
+ String entryName = entry.getKey();
+ if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey())
+ && isDebuggable(entryName)) {
+
+ V1SchemeVerifier.NamedDigest extractedDigest = null;
+ Collection<V1SchemeVerifier.NamedDigest> digestsToVerify =
+ V1SchemeVerifier.getDigestsToVerify(
+ entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE);
+ for (V1SchemeVerifier.NamedDigest digestToVerify : digestsToVerify) {
+ if (digestToVerify.jcaDigestAlgorithm.equals(alg)) {
+ extractedDigest = digestToVerify;
+ break;
+ }
+ }
+ if (extractedDigest != null) {
+ mOutputJarEntryDigests.put(entryName, extractedDigest.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) {
+ boolean schemeSignatureBlockPreserved = false;
+ mPreservedSignatureBlocks = new ArrayList<>();
+ try {
+ List<Pair<byte[], Integer>> signatureBlocks =
+ ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock);
+ for (Pair<byte[], Integer> signatureBlock : signatureBlocks) {
+ if (signatureBlock.getSecond() == Constants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
+ // If a V2 signature block is found and the engine is configured to use V2
+ // then save any of the previous signers that are not part of the current
+ // signing request.
+ if (mV2SigningEnabled) {
+ List<Pair<List<X509Certificate>, byte[]>> v2Signers =
+ ApkSigningBlockUtils.getApkSignatureBlockSigners(
+ signatureBlock.getFirst());
+ mPreservedV2Signers = new ArrayList<>(v2Signers.size());
+ for (Pair<List<X509Certificate>, byte[]> v2Signer : v2Signers) {
+ if (!isConfiguredWithSigner(v2Signer.getFirst())) {
+ mPreservedV2Signers.add(v2Signer.getSecond());
+ schemeSignatureBlockPreserved = true;
+ }
+ }
+ } else {
+ // else V2 signing is not enabled; save the entire signature block to be
+ // added to the final APK signing block.
+ mPreservedSignatureBlocks.add(signatureBlock);
+ schemeSignatureBlockPreserved = true;
+ }
+ } else if (signatureBlock.getSecond()
+ == Constants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
+ // Preserving other signers in the presence of a V3 signature block is only
+ // supported if the engine is configured to resign the APK with the V3
+ // signature scheme, and the V3 signer in the signature block is the same
+ // as the engine is configured to use.
+ if (!mV3SigningEnabled) {
+ throw new IllegalStateException(
+ "Preserving an existing V3 signature is not supported");
+ }
+ List<Pair<List<X509Certificate>, byte[]>> v3Signers =
+ ApkSigningBlockUtils.getApkSignatureBlockSigners(
+ signatureBlock.getFirst());
+ if (v3Signers.size() > 1) {
+ throw new IllegalArgumentException(
+ "The provided APK signing block contains " + v3Signers.size()
+ + " V3 signers; the V3 signature scheme only supports"
+ + " one signer");
+ }
+ // If there is only a single V3 signer then ensure it is the signer
+ // configured to sign the APK.
+ if (v3Signers.size() == 1
+ && !isConfiguredWithSigner(v3Signers.get(0).getFirst())) {
+ throw new IllegalStateException(
+ "The V3 signature scheme only supports one signer; a request "
+ + "was made to preserve the existing V3 signature, "
+ + "but the engine is configured to sign with a "
+ + "different signer");
+ }
+ } else if (!DISCARDED_SIGNATURE_BLOCK_IDS.contains(
+ signatureBlock.getSecond())) {
+ mPreservedSignatureBlocks.add(signatureBlock);
+ }
+ }
+ } catch (ApkFormatException | CertificateException | IOException e) {
+ throw new IllegalArgumentException("Unable to parse the provided signing block", e);
+ }
+ // Signature scheme V3+ only support a single signer; if the engine is configured to
+ // sign with V3+ then ensure no scheme signature blocks have been preserved.
+ if (mV3SigningEnabled && schemeSignatureBlockPreserved) {
+ throw new IllegalStateException(
+ "Signature scheme V3+ only supports a single signer and cannot be "
+ + "appended to the existing signature scheme blocks");
+ }
+ return;
+ }
+ }
+
+ /**
+ * Returns whether the engine is configured to sign the APK with a signer using the specified
+ * {@code signerCerts}.
+ */
+ private boolean isConfiguredWithSigner(List<X509Certificate> signerCerts) {
+ for (SignerConfig signerConfig : mSignerConfigs) {
+ if (signerCerts.containsAll(signerConfig.getCertificates())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @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 (V1SchemeConstants.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 (V1SchemeConstants.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());
+ }
+ if (isEligibleForSourceStamp()) {
+ MessageDigest messageDigest =
+ MessageDigest.getInstance(
+ V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
+ messageDigest.update(generateSourceStampCertificateDigest());
+ mOutputJarEntryDigests.put(
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, messageDigest.digest());
+ }
+ mOutputJarEntryDigestRequests.clear();
+
+ for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
+ if (!dataRequest.isDone()) {
+ throw new IllegalStateException(
+ "Still waiting to inspect output APK's " + dataRequest.getEntryName());
+ }
+ }
+
+ List<Integer> 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;
+ if (isEligibleForSourceStamp()) {
+ inputJarManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ inputJarManifest)
+ .contents;
+ }
+
+ // Check whether the most recently used signature (if present) is still fine.
+ checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
+ List<Pair<String, byte[]>> 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(V1SchemeConstants.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<String, byte[]> 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<OutputJarSignatureRequest.JarEntry> sigEntries =
+ new ArrayList<>(signatureZipEntries.size());
+ for (Pair<String, byte[]> 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 && !isEligibleForSourceStamp()) {
+ return null;
+ }
+ checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
+
+ // adjust to proper padding
+ Pair<DataSource, Integer> paddingPair =
+ ApkSigningBlockUtils.generateApkSigningBlockPadding(
+ zipEntries, apkSigningBlockPaddingSupported);
+ DataSource beforeCentralDir = paddingPair.getFirst();
+ int padSizeBeforeApkSigningBlock = paddingPair.getSecond();
+ DataSource eocd = ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd);
+
+ List<Pair<byte[], Integer>> signingSchemeBlocks = new ArrayList<>();
+ ApkSigningBlockUtils.SigningSchemeBlockAndDigests v2SigningSchemeBlockAndDigests = null;
+ ApkSigningBlockUtils.SigningSchemeBlockAndDigests v3SigningSchemeBlockAndDigests = null;
+ // If the engine is configured to preserve previous signature blocks and any were found in
+ // the existing APK signing block then add them to the list to be used to generate the
+ // new APK signing block.
+ if (mOtherSignersSignaturesPreserved && mPreservedSignatureBlocks != null
+ && !mPreservedSignatureBlocks.isEmpty()) {
+ signingSchemeBlocks.addAll(mPreservedSignatureBlocks);
+ }
+
+ // create APK Signature Scheme V2 Signature if requested
+ if (mV2SigningEnabled) {
+ invalidateV2Signature();
+ List<ApkSigningBlockUtils.SignerConfig> v2SignerConfigs =
+ createV2SignerConfigs(apkSigningBlockPaddingSupported);
+ v2SigningSchemeBlockAndDigests =
+ V2SchemeSigner.generateApkSignatureSchemeV2Block(
+ mExecutor,
+ beforeCentralDir,
+ zipCentralDirectory,
+ eocd,
+ v2SignerConfigs,
+ mV3SigningEnabled,
+ mOtherSignersSignaturesPreserved ? mPreservedV2Signers : null);
+ signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock);
+ }
+ if (mV3SigningEnabled) {
+ invalidateV3Signature();
+ List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs =
+ createV3SignerConfigs(apkSigningBlockPaddingSupported);
+ List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = processV31SignerConfigs(
+ v3SignerConfigs);
+ if (v31SignerConfigs != null && v31SignerConfigs.size() > 0) {
+ ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+ v31SigningSchemeBlockAndDigests =
+ new V3SchemeSigner.Builder(beforeCentralDir, zipCentralDirectory, eocd,
+ v31SignerConfigs)
+ .setRunnablesExecutor(mExecutor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
+ .build()
+ .generateApkSignatureSchemeV3BlockAndDigests();
+ signingSchemeBlocks.add(v31SigningSchemeBlockAndDigests.signingSchemeBlock);
+ }
+ V3SchemeSigner.Builder builder = new V3SchemeSigner.Builder(beforeCentralDir,
+ zipCentralDirectory, eocd, v3SignerConfigs)
+ .setRunnablesExecutor(mExecutor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ if (v31SignerConfigs != null && !v31SignerConfigs.isEmpty()) {
+ // The V3.1 stripping protection writes the minimum SDK version from the targeted
+ // signers as an additional attribute in the V3.0 signing block.
+ int minSdkVersionForV31 = v31SignerConfigs.stream().mapToInt(
+ signer -> signer.minSdkVersion).min().orElse(MIN_SDK_WITH_V31_SUPPORT);
+ builder.setMinSdkVersionForV31(minSdkVersionForV31);
+ }
+ v3SigningSchemeBlockAndDigests =
+ builder.build().generateApkSignatureSchemeV3BlockAndDigests();
+ signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock);
+ }
+ if (isEligibleForSourceStamp()) {
+ ApkSigningBlockUtils.SignerConfig sourceStampSignerConfig =
+ createSourceStampSignerConfig();
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos =
+ new HashMap<>();
+ if (mV3SigningEnabled) {
+ signatureSchemeDigestInfos.put(
+ VERSION_APK_SIGNATURE_SCHEME_V3, v3SigningSchemeBlockAndDigests.digestInfo);
+ }
+ if (mV2SigningEnabled) {
+ signatureSchemeDigestInfos.put(
+ VERSION_APK_SIGNATURE_SCHEME_V2, v2SigningSchemeBlockAndDigests.digestInfo);
+ }
+ if (mV1SigningEnabled) {
+ Map<ContentDigestAlgorithm, byte[]> v1SigningSchemeDigests = new HashMap<>();
+ try {
+ // Jar signing related variables must have been already populated at this point
+ // if V1 signing is enabled since it is happening before computations on the APK
+ // signing block (V2/V3/V4/SourceStamp signing).
+ byte[] inputJarManifest =
+ (mInputJarManifestEntryDataRequest != null)
+ ? mInputJarManifestEntryDataRequest.getData()
+ : null;
+ byte[] jarManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ inputJarManifest)
+ .contents;
+ // The digest of the jar manifest does not need to be computed in chunks due to
+ // the small size of the manifest.
+ v1SigningSchemeDigests.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(jarManifest));
+ } catch (ApkFormatException e) {
+ throw new RuntimeException("Failed to generate manifest file", e);
+ }
+ signatureSchemeDigestInfos.put(
+ VERSION_JAR_SIGNATURE_SCHEME, v1SigningSchemeDigests);
+ }
+ V2SourceStampSigner v2SourceStampSigner =
+ new V2SourceStampSigner.Builder(sourceStampSignerConfig,
+ signatureSchemeDigestInfos)
+ .setSourceStampTimestampEnabled(mSourceStampTimestampEnabled)
+ .build();
+ signingSchemeBlocks.add(v2SourceStampSigner.generateSourceStampBlock());
+ }
+
+ // create APK Signing Block with v2 and/or v3 and/or SourceStamp blocks
+ byte[] apkSigningBlock = ApkSigningBlockUtils.generateApkSigningBlock(signingSchemeBlocks);
+
+ mAddSigningBlockRequest =
+ new OutputApkSigningBlockRequestImpl(apkSigningBlock, padSizeBeforeApkSigningBlock);
+ return mAddSigningBlockRequest;
+ }
+
+ @Override
+ public void outputDone() {
+ checkNotClosed();
+ checkV1SigningDoneIfEnabled();
+ checkSigningBlockDoneIfEnabled();
+ }
+
+ @Override
+ public void signV4(DataSource dataSource, File outputFile, boolean ignoreFailures)
+ throws SignatureException {
+ if (outputFile == null) {
+ if (ignoreFailures) {
+ return;
+ }
+ throw new SignatureException("Missing V4 output file.");
+ }
+ try {
+ V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig();
+ V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile);
+ } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+ if (ignoreFailures) {
+ return;
+ }
+ throw new SignatureException("V4 signing failed", e);
+ }
+ }
+
+ /** For external use only to generate V4 & tree separately. */
+ public byte[] produceV4Signature(DataSource dataSource, OutputStream sigOutput)
+ throws SignatureException {
+ if (sigOutput == null) {
+ throw new SignatureException("Missing V4 output streams.");
+ }
+ try {
+ V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig();
+ Pair<V4Signature, byte[]> pair =
+ V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig);
+ pair.getFirst().writeTo(sigOutput);
+ return pair.getSecond();
+ } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) {
+ throw new SignatureException("V4 signing failed", e);
+ }
+ }
+
+ @Override
+ public boolean isEligibleForSourceStamp() {
+ return mSourceStampSignerConfig != null
+ && (mV2SigningEnabled || mV3SigningEnabled || mV1SigningEnabled);
+ }
+
+ @Override
+ public byte[] generateSourceStampCertificateDigest() throws SignatureException {
+ if (mSourceStampSignerConfig.getCertificates().isEmpty()) {
+ throw new SignatureException("No certificates configured for stamp");
+ }
+ try {
+ return computeSha256DigestBytes(
+ mSourceStampSignerConfig.getCertificates().get(0).getEncoded());
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException("Failed to encode source stamp certificate", e);
+ }
+ }
+
+ @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<String, byte[]> 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<JarEntry> mAdditionalJarEntries;
+ private volatile boolean mDone;
+
+ private OutputJarSignatureRequestImpl(List<JarEntry> additionalZipEntries) {
+ mAdditionalJarEntries =
+ Collections.unmodifiableList(new ArrayList<>(additionalZipEntries));
+ }
+
+ @Override
+ public List<JarEntry> 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.
+ *
+ * <p>Use {@link Builder} to obtain configuration instances.
+ */
+ public static class SignerConfig {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List<X509Certificate> mCertificates;
+ private final boolean mDeterministicDsaSigning;
+ private final int mMinSdkVersion;
+ private final boolean mSignerTargetsDevRelease;
+ private final SigningCertificateLineage mSigningCertificateLineage;
+
+ private SignerConfig(Builder builder) {
+ mName = builder.mName;
+ mPrivateKey = builder.mPrivateKey;
+ mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates));
+ mDeterministicDsaSigning = builder.mDeterministicDsaSigning;
+ mMinSdkVersion = builder.mMinSdkVersion;
+ mSignerTargetsDevRelease = builder.mSignerTargetsDevRelease;
+ mSigningCertificateLineage = builder.mSigningCertificateLineage;
+ }
+
+ /** 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<X509Certificate> getCertificates() {
+ return mCertificates;
+ }
+
+ /**
+ * If this signer is a DSA signer, whether or not the signing is done deterministically.
+ */
+ public boolean getDeterministicDsaSigning() {
+ return mDeterministicDsaSigning;
+ }
+
+ /** Returns the minimum SDK version for which this signer should be used. */
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ /** Returns whether this signer targets a development release. */
+ public boolean getSignerTargetsDevRelease() {
+ return mSignerTargetsDevRelease;
+ }
+
+ /** Returns the {@link SigningCertificateLineage} for this signer. */
+ public SigningCertificateLineage getSigningCertificateLineage() {
+ return mSigningCertificateLineage;
+ }
+
+ /** Builder of {@link SignerConfig} instances. */
+ public static class Builder {
+ private final String mName;
+ private final PrivateKey mPrivateKey;
+ private final List<X509Certificate> mCertificates;
+ private final boolean mDeterministicDsaSigning;
+ private int mMinSdkVersion;
+ private boolean mSignerTargetsDevRelease;
+ private SigningCertificateLineage mSigningCertificateLineage;
+
+ /**
+ * 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<X509Certificate> certificates) {
+ this(name, privateKey, certificates, false);
+ }
+
+ /**
+ * 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}.
+ * @param deterministicDsaSigning When signing using DSA, whether or not the
+ * deterministic signing algorithm variant (RFC6979) should be used.
+ */
+ public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates,
+ boolean deterministicDsaSigning) {
+ if (name.isEmpty()) {
+ throw new IllegalArgumentException("Empty name");
+ }
+ mName = name;
+ mPrivateKey = privateKey;
+ mCertificates = new ArrayList<>(certificates);
+ mDeterministicDsaSigning = deterministicDsaSigning;
+ }
+
+ /** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */
+ public Builder setMinSdkVersion(int minSdkVersion) {
+ return setLineageForMinSdkVersion(null, minSdkVersion);
+ }
+
+ /**
+ * Sets the specified {@code minSdkVersion} as the minimum Android platform version
+ * (API level) for which the provided {@code lineage} (where applicable) should be used
+ * to produce the APK's signature. This method is useful if callers want to specify a
+ * particular rotated signer or lineage with restricted capabilities for later
+ * platform releases.
+ *
+ * <p><em>Note:</em>>The V1 and V2 signature schemes do not support key rotation and
+ * signing lineages with capabilities; only an app's original signer(s) can be used for
+ * the V1 and V2 signature blocks. Because of this, only a value of {@code
+ * minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was
+ * introduced can be specified.
+ *
+ * <p><em>Note:</em>Due to limitations with platform targeting in the V3.0 signature
+ * scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in
+ * the current {@code SignerConfig} being used in the V3.0 signing block and applied to
+ * Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for
+ * subsequent {@code SignerConfig} instances). Because of this, only a single {@code
+ * SignerConfig} can be instantiated with a minimum SDK version <= 32.
+ *
+ * @param lineage the {@code SigningCertificateLineage} to target the specified {@code
+ * minSdkVersion}
+ * @param minSdkVersion the minimum SDK version for which this {@code SignerConfig}
+ * should be used
+ * @return this {@code Builder} instance
+ *
+ * @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the
+ * certificate provided in the constructor is not in the specified {@code lineage}.
+ */
+ public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage,
+ int minSdkVersion) {
+ if (minSdkVersion < AndroidSdkVersion.P) {
+ throw new IllegalArgumentException(
+ "SDK targeted signing config is only supported with the V3 signature "
+ + "scheme on Android P (SDK version "
+ + AndroidSdkVersion.P + ") and later");
+ }
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ minSdkVersion = AndroidSdkVersion.P;
+ }
+ mMinSdkVersion = minSdkVersion;
+ // If a lineage is provided, ensure the signing certificate for this signer is in
+ // the lineage; in the case of multiple signing certificates, the first is always
+ // used in the lineage.
+ if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) {
+ throw new IllegalArgumentException(
+ "The provided lineage does not contain the signing certificate, "
+ + mCertificates.get(0).getSubjectDN()
+ + ", for this SignerConfig");
+ }
+ mSigningCertificateLineage = lineage;
+ return this;
+ }
+
+ /**
+ * Sets whether this signer's min SDK version is intended to target a development
+ * release.
+ *
+ * <p>This is primarily required for a signer testing on a platform's development
+ * release; however, it is recommended that signer's use the latest development SDK
+ * version instead of explicitly specifying this boolean. This class will properly
+ * handle an SDK that is currently targeting a development release and will use the
+ * finalized SDK version on release.
+ */
+ private Builder setSignerTargetsDevRelease(boolean signerTargetsDevRelease) {
+ if (signerTargetsDevRelease && mMinSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ throw new IllegalArgumentException(
+ "Rotation can only target a development release for signers targeting "
+ + MIN_SDK_WITH_V31_SUPPORT + " or later");
+ }
+ mSignerTargetsDevRelease = signerTargetsDevRelease;
+ return this;
+ }
+
+
+ /**
+ * Returns a new {@code SignerConfig} instance configured based on the configuration of
+ * this builder.
+ */
+ public SignerConfig build() {
+ return new SignerConfig(this);
+ }
+ }
+ }
+
+ /** Builder of {@link DefaultApkSignerEngine} instances. */
+ public static class Builder {
+ private List<SignerConfig> mSignerConfigs;
+ private List<SignerConfig> mTargetedSignerConfigs;
+ private SignerConfig mStampSignerConfig;
+ private SigningCertificateLineage mSourceStampSigningCertificateLineage;
+ private boolean mSourceStampTimestampEnabled = true;
+ private final int mMinSdkVersion;
+
+ private boolean mV1SigningEnabled = true;
+ private boolean mV2SigningEnabled = true;
+ private boolean mV3SigningEnabled = true;
+ private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION;
+ private boolean mRotationTargetsDevRelease = false;
+ private boolean mVerityEnabled = false;
+ 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<SignerConfig> 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;
+ }
+
+ /**
+ * Sets the APK signature schemes that should be enabled based on the options provided by
+ * the caller.
+ */
+ private void setEnabledSignatureSchemes() {
+ 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;
+ }
+ }
+
+ /**
+ * Sets the SDK targeted signer configs based on the signing config and rotation options
+ * provided by the caller.
+ *
+ * @throws InvalidKeyException if a {@link SigningCertificateLineage} cannot be created
+ * from the provided options
+ */
+ private void setTargetedSignerConfigs() throws InvalidKeyException {
+ // If the caller specified any SDK targeted signer configs, then the min SDK version
+ // should be set for those configs, all others should have a default 0 min SDK version.
+ mSignerConfigs.sort(((signerConfig1, signerConfig2) -> signerConfig1.getMinSdkVersion()
+ - signerConfig2.getMinSdkVersion()));
+ // With the signer configs sorted, find the first targeted signer config with a min
+ // SDK version > 0 to create the separate targeted signer configs.
+ mTargetedSignerConfigs = new ArrayList<>();
+ for (int i = 0; i < mSignerConfigs.size(); i++) {
+ if (mSignerConfigs.get(i).getMinSdkVersion() > 0) {
+ mTargetedSignerConfigs = mSignerConfigs.subList(i, mSignerConfigs.size());
+ mSignerConfigs = mSignerConfigs.subList(0, i);
+ break;
+ }
+ }
+
+ // A lineage provided outside a targeted signing config is intended for the original
+ // rotation; sort the untargeted signing configs based on this lineage and create a new
+ // targeted signing config for the initial rotation.
+ if (mSigningCertificateLineage != null) {
+ if (!mTargetedSignerConfigs.isEmpty()) {
+ // Only the initial rotation can use the rotation-min-sdk-version; all
+ // subsequent targeted rotations must use targeted signing configs.
+ int firstTargetedSdkVersion = mTargetedSignerConfigs.get(0).getMinSdkVersion();
+ if (mRotationMinSdkVersion >= firstTargetedSdkVersion) {
+ throw new IllegalStateException(
+ "The rotation-min-sdk-version, " + mRotationMinSdkVersion
+ + ", must be less than the first targeted SDK version, "
+ + firstTargetedSdkVersion);
+ }
+ }
+ try {
+ mSignerConfigs = mSigningCertificateLineage.sortSignerConfigs(mSignerConfigs);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalStateException(
+ "Provided signer configs do not match the "
+ + "provided SigningCertificateLineage",
+ e);
+ }
+ // Get the last signer in the lineage, create a new targeted signer from it,
+ // and add it as a targeted signer config.
+ SignerConfig rotatedSignerConfig = mSignerConfigs.remove(mSignerConfigs.size() - 1);
+ SignerConfig.Builder rotatedConfigBuilder = new SignerConfig.Builder(
+ rotatedSignerConfig.getName(), rotatedSignerConfig.getPrivateKey(),
+ rotatedSignerConfig.getCertificates(),
+ rotatedSignerConfig.getDeterministicDsaSigning());
+ rotatedConfigBuilder.setLineageForMinSdkVersion(mSigningCertificateLineage,
+ mRotationMinSdkVersion);
+ rotatedConfigBuilder.setSignerTargetsDevRelease(mRotationTargetsDevRelease);
+ mTargetedSignerConfigs.add(0, rotatedConfigBuilder.build());
+ }
+ mSigningCertificateLineage = mergeTargetedSigningConfigLineages();
+ }
+
+ /**
+ * Merges and returns the lineages from any caller provided SDK targeted {@link
+ * SignerConfig} instances with an optional {@code lineage} specified as part of the general
+ * signing config.
+ *
+ * <p>If multiple signing configs target the same SDK version, or if any of the lineages
+ * cannot be merged, then an {@code IllegalStateException} is thrown.
+ */
+ private SigningCertificateLineage mergeTargetedSigningConfigLineages()
+ throws InvalidKeyException {
+ SigningCertificateLineage mergedLineage = null;
+ int prevSdkVersion = 0;
+ for (SignerConfig signerConfig : mTargetedSignerConfigs) {
+ int signerMinSdkVersion = signerConfig.getMinSdkVersion();
+ if (signerMinSdkVersion < AndroidSdkVersion.P) {
+ throw new IllegalStateException(
+ "Targeted signing config is not supported prior to SDK version "
+ + AndroidSdkVersion.P + "; received value "
+ + signerMinSdkVersion);
+ }
+ SigningCertificateLineage signerLineage =
+ signerConfig.getSigningCertificateLineage();
+ // It is possible for a lineage to be null if the user is using one of the
+ // signers from the lineage as the only signer to target an SDK version; create
+ // a single element lineage to verify the signer is part of the merged lineage.
+ if (signerLineage == null) {
+ try {
+ signerLineage = new SigningCertificateLineage.Builder(
+ new SigningCertificateLineage.SignerConfig.Builder(
+ signerConfig.mPrivateKey,
+ signerConfig.mCertificates.get(0))
+ .build())
+ .build();
+ } catch (CertificateEncodingException | NoSuchAlgorithmException
+ | SignatureException e) {
+ throw new IllegalStateException(
+ "Unable to create a SignerConfig for signer from certificate "
+ + signerConfig.mCertificates.get(0).getSubjectDN());
+ }
+ }
+ // The V3.0 signature scheme does not support verified targeted SDK signing
+ // configs; if a signer is targeting any SDK version < T, then it will
+ // target P with the V3.0 signature scheme.
+ if (signerMinSdkVersion < AndroidSdkVersion.T) {
+ signerMinSdkVersion = AndroidSdkVersion.P;
+ }
+ // Ensure there are no SignerConfigs targeting the same SDK version.
+ if (signerMinSdkVersion == prevSdkVersion) {
+ throw new IllegalStateException(
+ "Multiple SignerConfigs were found targeting SDK version "
+ + signerMinSdkVersion);
+ }
+ // If multiple lineages have been provided, then verify each subsequent lineage
+ // is a valid descendant or ancestor of the previously merged lineages.
+ if (mergedLineage == null) {
+ mergedLineage = signerLineage;
+ } else {
+ try {
+ mergedLineage = mergedLineage.mergeLineageWith(signerLineage);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalStateException(
+ "The provided lineage targeting SDK " + signerMinSdkVersion
+ + " is not in the signing history of the other targeted "
+ + "signing configs", e);
+ }
+ }
+ prevSdkVersion = signerMinSdkVersion;
+ }
+ return mergedLineage;
+ }
+
+ /**
+ * Returns a new {@code DefaultApkSignerEngine} instance configured based on the
+ * configuration of this builder.
+ */
+ public DefaultApkSignerEngine build() throws InvalidKeyException {
+ setEnabledSignatureSchemes();
+ setTargetedSignerConfigs();
+
+ // make sure our signers are appropriately setup
+ if (mSigningCertificateLineage != null) {
+ 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");
+ }
+ } 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,
+ mTargetedSignerConfigs,
+ mStampSignerConfig,
+ mSourceStampSigningCertificateLineage,
+ mSourceStampTimestampEnabled,
+ mMinSdkVersion,
+ mV1SigningEnabled,
+ mV2SigningEnabled,
+ mV3SigningEnabled,
+ mVerityEnabled,
+ mDebuggableApkPermitted,
+ mOtherSignersSignaturesPreserved,
+ mCreatedBy,
+ mSigningCertificateLineage);
+ }
+
+ /** Sets the signer configuration for the SourceStamp to be embedded in the APK. */
+ public Builder setStampSignerConfig(SignerConfig stampSignerConfig) {
+ mStampSignerConfig = stampSignerConfig;
+ return this;
+ }
+
+ /**
+ * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of
+ * signing certificate rotation for certificates previously used to sign source stamps.
+ */
+ public Builder setSourceStampSigningCertificateLineage(
+ SigningCertificateLineage sourceStampSigningCertificateLineage) {
+ mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage;
+ return this;
+ }
+
+ /**
+ * Sets whether the source stamp should contain the timestamp attribute with the time
+ * at which the source stamp was signed.
+ */
+ public Builder setSourceStampTimestampEnabled(boolean value) {
+ mSourceStampTimestampEnabled = value;
+ return this;
+ }
+
+ /**
+ * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
+ *
+ * <p>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).
+ *
+ * <p>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).
+ *
+ * <p>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 using the verity signature algorithm in the v2 and
+ * v3 signature blocks.
+ *
+ * <p>By default, the APK will be signed using the verity signature algorithm for the v2 and
+ * v3 signature schemes.
+ */
+ public Builder setVerityEnabled(boolean enabled) {
+ mVerityEnabled = enabled;
+ 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}.
+ *
+ * <p>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.
+ *
+ * <p>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;
+ }
+
+ /**
+ * Sets the minimum Android platform version (API Level) for which an APK's rotated signing
+ * key should be used to produce the APK's signature. The original signing key for the APK
+ * will be used for all previous platform versions. If a rotated key with signing lineage is
+ * not provided then this method is a noop.
+ *
+ * <p>By default, if a signing lineage is specified with {@link
+ * #setSigningCertificateLineage(SigningCertificateLineage)}, then the APK Signature Scheme
+ * V3.1 will be used to only apply the rotation on devices running Android T+.
+ *
+ * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result
+ * in the original V3 signing block being used without platform targeting.
+ */
+ public Builder setMinSdkVersionForRotation(int minSdkVersion) {
+ // If the provided SDK version does not support v3.1, then use the default SDK version
+ // with rotation support.
+ if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) {
+ mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT;
+ } else {
+ mRotationMinSdkVersion = minSdkVersion;
+ }
+ return this;
+ }
+
+ /**
+ * Sets whether the rotation-min-sdk-version is intended to target a development release;
+ * this is primarily required after the T SDK is finalized, and an APK needs to target U
+ * during its development cycle for rotation.
+ *
+ * <p>This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/Hints.java b/platform/android/java/editor/src/main/java/com/android/apksig/Hints.java
new file mode 100644
index 0000000000..4070fa231a
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/Hints.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.apksig;
+import java.io.IOException;
+import java.io.DataOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class Hints {
+ /**
+ * Name of hint pattern asset file in APK.
+ */
+ public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
+
+ /**
+ * Name of hint byte range data file in APK. Keep in sync with PinnerService.java.
+ */
+ public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
+
+ private static int clampToInt(long value) {
+ return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
+ }
+
+ public static final class ByteRange {
+ final long start;
+ final long end;
+
+ public ByteRange(long start, long end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+
+ public static final class PatternWithRange {
+ final Pattern pattern;
+ final long offset;
+ final long size;
+
+ public PatternWithRange(String pattern) {
+ this.pattern = Pattern.compile(pattern);
+ this.offset= 0;
+ this.size = Long.MAX_VALUE;
+ }
+
+ public PatternWithRange(String pattern, long offset, long size) {
+ this.pattern = Pattern.compile(pattern);
+ this.offset = offset;
+ this.size = size;
+ }
+
+ public Matcher matcher(CharSequence input) {
+ return this.pattern.matcher(input);
+ }
+
+ public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) {
+ if (rangeIn.end - rangeIn.start < this.offset) {
+ return null;
+ }
+ long rangeOutStart = rangeIn.start + this.offset;
+ long rangeOutSize = Math.min(rangeIn.end - rangeOutStart,
+ this.size);
+ return new ByteRange(rangeOutStart,
+ rangeOutStart + rangeOutSize);
+ }
+ }
+
+ /**
+ * Create a blob of bytes that PinnerService understands as a
+ * sequence of byte ranges to pin.
+ */
+ public static byte[] encodeByteRangeList(List<ByteRange> pinByteRanges) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8);
+ DataOutputStream out = new DataOutputStream(bos);
+ try {
+ for (ByteRange pinByteRange : pinByteRanges) {
+ out.writeInt(clampToInt(pinByteRange.start));
+ out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start));
+ }
+ } catch (IOException ex) {
+ throw new AssertionError("impossible", ex);
+ }
+ return bos.toByteArray();
+ }
+
+ public static ArrayList<PatternWithRange> parsePinPatterns(byte[] patternBlob) {
+ ArrayList<PatternWithRange> pinPatterns = new ArrayList<>();
+ try {
+ for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
+ String line = rawLine.replaceFirst("#.*", ""); // # starts a comment
+ String[] fields = line.split(" ");
+ if (fields.length == 1) {
+ pinPatterns.add(new PatternWithRange(fields[0]));
+ } else if (fields.length == 3) {
+ long start = Long.parseLong(fields[1]);
+ long end = Long.parseLong(fields[2]);
+ pinPatterns.add(new PatternWithRange(fields[0], start, end - start));
+ } else {
+ throw new AssertionError("bad pin pattern line " + line);
+ }
+ }
+ } catch (UnsupportedEncodingException ex) {
+ throw new RuntimeException("UTF-8 must be supported", ex);
+ }
+ return pinPatterns;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/README.md b/platform/android/java/editor/src/main/java/com/android/apksig/README.md
new file mode 100644
index 0000000000..a89b848909
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/README.md
@@ -0,0 +1,32 @@
+# apksig ([commit ac5cbb07d87cc342fcf07715857a812305d69888](https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888))
+
+apksig is a project which aims to simplify APK signing and checking whether APK signatures are
+expected to verify on Android. apksig supports
+[JAR signing](https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File)
+(used by Android since day one) and
+[APK Signature Scheme v2](https://source.android.com/security/apksigning/v2.html) (supported since
+Android Nougat, API Level 24). apksig is meant to be used outside of Android devices.
+
+The key feature of apksig is that it knows about differences in APK signature verification logic
+between different versions of the Android platform. apksig thus thoroughly checks whether an APK's
+signature is expected to verify on all Android platform versions supported by the APK. When signing
+an APK, apksig chooses the most appropriate cryptographic algorithms based on the Android platform
+versions supported by the APK being signed.
+
+## apksig library
+
+apksig library offers three primitives:
+
+* `ApkSigner` which signs the provided APK so that it verifies on all Android platform versions
+ supported by the APK. The range of platform versions can be customized.
+* `ApkVerifier` which checks whether the provided APK is expected to verify on all Android
+ platform versions supported by the APK. The range of platform versions can be customized.
+* `(Default)ApkSignerEngine` which abstracts away signing APKs from parsing and building APKs.
+ This is useful in optimized APK building pipelines, such as in Android Plugin for Gradle,
+ which need to perform signing while building an APK, instead of after. For simpler use cases
+ where the APK to be signed is available upfront, the `ApkSigner` above is easier to use.
+
+_NOTE: Some public classes of the library are in packages having the word "internal" in their name.
+These are not public API of the library. Do not use \*.internal.\* classes directly because these
+classes may change any time without regard to existing clients outside of `apksig` and `apksigner`._
+
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/SigningCertificateLineage.java b/platform/android/java/editor/src/main/java/com/android/apksig/SigningCertificateLineage.java
new file mode 100644
index 0000000000..0f1cc33c98
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/SigningCertificateLineage.java
@@ -0,0 +1,1325 @@
+/*
+ * 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.V3SchemeConstants;
+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.
+ *
+ * <p>The signer lineage contains a history of signing certificates with each ancestor attesting to
+ * the validity of its descendant. Each additional descendant represents a new identity that can be
+ * used to sign an APK, and each generation has accompanying attributes which represent how the
+ * APK would like to view the older signing certificates, specifically how they should be trusted in
+ * certain situations.
+ *
+ * <p> Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies
+ * the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
+ * Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
+ * allow upgrades to the new certificate.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
+ */
+public class 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<SigningCertificateNode> mSigningLineage;
+
+ private SigningCertificateLineage(int minSdkVersion, List<SigningCertificateNode> list) {
+ mMinSdkVersion = minSdkVersion;
+ mSigningLineage = list;
+ }
+
+ /**
+ * Creates a {@code SigningCertificateLineage} with a single signer in the lineage.
+ */
+ private static SigningCertificateLineage createSigningLineage(int minSdkVersion,
+ SignerConfig signer, SignerCapabilities capabilities) {
+ SigningCertificateLineage signingCertificateLineage = new SigningCertificateLineage(
+ minSdkVersion, new ArrayList<>());
+ return signingCertificateLineage.spawnFirstDescendant(signer, capabilities);
+ }
+
+ 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 readFromBytes(byte[] lineageBytes)
+ throws IOException {
+ return readFromDataSource(DataSources.asDataSource(ByteBuffer.wrap(lineageBytes)));
+ }
+
+ 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.
+ *
+ * <note>
+ * 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 <code> readFromApkFile</code> to handle this case.
+ * </note>
+ * @param attrValue
+ */
+ public static SigningCertificateLineage readFromV3AttributeValue(byte[] attrValue)
+ throws IOException {
+ List<SigningCertificateNode> 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 and
+ * V3.1 signature blocks of the provided APK DataSource.
+ *
+ * @throws IllegalArgumentException if the provided APK does not contain a V3 nor V3.1
+ * signature block, or if the V3 and V3.1 signature blocks do not contain a valid lineage.
+ */
+
+ public static SigningCertificateLineage readFromApkDataSource(DataSource apk)
+ throws IOException, ApkFormatException {
+ return readFromApkDataSource(apk, /* readV31Lineage= */ true, /* readV3Lineage= */true);
+ }
+
+ /**
+ * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3.1
+ * signature blocks of the provided APK DataSource.
+ *
+ * @throws IllegalArgumentException if the provided APK does not contain a V3.1 signature block,
+ * or if the V3.1 signature block does not contain a valid lineage.
+ */
+
+ public static SigningCertificateLineage readV31FromApkDataSource(DataSource apk)
+ throws IOException, ApkFormatException {
+ return readFromApkDataSource(apk, /* readV31Lineage= */ true,
+ /* readV3Lineage= */ false);
+ }
+
+ private static SigningCertificateLineage readFromApkDataSource(
+ DataSource apk,
+ boolean readV31Lineage,
+ boolean readV3Lineage)
+ throws IOException, ApkFormatException {
+ ApkUtils.ZipSections zipSections;
+ try {
+ zipSections = ApkUtils.findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException(e.getMessage());
+ }
+
+ List<SignatureInfo> signatureInfoList = new ArrayList<>();
+ if (readV31Lineage) {
+ try {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31);
+ signatureInfoList.add(
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result));
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // This could be expected if there's only a V3 signature block.
+ }
+ }
+ if (readV3Lineage) {
+ try {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ signatureInfoList.add(
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result));
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ // This could be expected if the provided APK is not signed with the V3 signature
+ // scheme
+ }
+ }
+ if (signatureInfoList.isEmpty()) {
+ String message;
+ if (readV31Lineage && readV3Lineage) {
+ message = "The provided APK does not contain a valid V3 nor V3.1 signature block.";
+ } else if (readV31Lineage) {
+ message = "The provided APK does not contain a valid V3.1 signature block.";
+ } else if (readV3Lineage) {
+ message = "The provided APK does not contain a valid V3 signature block.";
+ } else {
+ message = "No signature blocks were requested.";
+ }
+ throw new IllegalArgumentException(message);
+ }
+
+ List<SigningCertificateLineage> lineages = new ArrayList<>(1);
+ for (SignatureInfo signatureInfo : signatureInfoList) {
+ // 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);
+ 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<SigningCertificateLineage> lineages = new ArrayList<>(1);
+ while (additionalAttributes.hasRemaining()) {
+ ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
+ int id = attribute.getInt();
+ if (id == V3SchemeConstants.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 byte[] getBytes() {
+ return write().array();
+ }
+
+ 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<X509Certificate> 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<Pair<Integer, byte[]>> 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<SigningCertificateNode> 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<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
+ publicKey, mMinSdkVersion, false /* verityEnabled */,
+ false /* deterministicDsaSigning */);
+ 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<SigningCertificateNode> 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<SigningCertificateNode> 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[] encodeSigningCertificateLineage() {
+ return V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage);
+ }
+
+ public List<DefaultApkSignerEngine.SignerConfig> sortSignerConfigs(
+ List<DefaultApkSignerEngine.SignerConfig> 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<DefaultApkSignerEngine.SignerConfig> 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");
+ }
+ updateSignerCapabilities(config.getCertificate(), capabilities);
+ }
+
+ /**
+ * Updates the {@code capabilities} for the signer with the provided {@code certificate} in the
+ * lineage. 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(X509Certificate certificate,
+ SignerCapabilities capabilities) {
+ if (certificate == null) {
+ throw new NullPointerException("config == null");
+ }
+
+ for (int i = 0; i < mSigningLineage.size(); i++) {
+ SigningCertificateNode lineageNode = mSigningLineage.get(i);
+ if (lineageNode.signingCert.equals(certificate)) {
+ 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 (" + certificate.getSubjectDN()
+ + ") not found in the SigningCertificateLineage");
+ }
+
+ /**
+ * Returns a list containing all of the certificates in the lineage.
+ */
+ public List<X509Certificate> getCertificatesInLineage() {
+ List<X509Certificate> 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;
+ }
+
+ /**
+ * Returns whether the provided {@code cert} is the latest signing certificate in the lineage.
+ *
+ * <p>This method will only compare the provided {@code cert} against the latest signing
+ * certificate in the lineage; if a certificate that is not in the lineage is provided, this
+ * method will return false.
+ */
+ public boolean isCertificateLatestInLineage(X509Certificate cert) {
+ if (cert == null) {
+ throw new NullPointerException("cert == null");
+ }
+
+ return mSigningLineage.get(mSigningLineage.size() - 1).signingCert.equals(cert);
+ }
+
+ private static int calculateDefaultFlags() {
+ return PAST_CERT_INSTALLED_DATA | PAST_CERT_PERMISSION
+ | PAST_CERT_SHARED_USER_ID | PAST_CERT_AUTH;
+ }
+
+ /**
+ * Returns a new SigningCertificateLineage 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. In so doing, it also
+ * checks that all of the lineages are contained in one common lineage.
+ *
+ * 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) should be present in the most recent, either directly or via a sublineage
+ * that would allow the earlier lineages to merge with the most recent.
+ *
+ * <note> This does not verify that the largest lineage corresponds to the most recent supported
+ * platform version. That check is performed during v3 verification. </note>
+ */
+ public static SigningCertificateLineage consolidateLineages(
+ List<SigningCertificateLineage> lineages) {
+ if (lineages == null || lineages.isEmpty()) {
+ return null;
+ }
+ SigningCertificateLineage consolidatedLineage = lineages.get(0);
+ for (int i = 1; i < lineages.size(); i++) {
+ consolidatedLineage = consolidatedLineage.mergeLineageWith(lineages.get(i));
+ }
+ return consolidatedLineage;
+ }
+
+ /**
+ * Merges this lineage with the provided {@code otherLineage}.
+ *
+ * <p>The merged lineage does not currently handle merging capabilities of common signers and
+ * should only be used to determine the full signing history of a collection of lineages.
+ */
+ public SigningCertificateLineage mergeLineageWith(SigningCertificateLineage otherLineage) {
+ // Determine the ancestor and descendant lineages; if the original signer is in the other
+ // lineage, then it is considered a descendant.
+ SigningCertificateLineage ancestorLineage;
+ SigningCertificateLineage descendantLineage;
+ X509Certificate signerCert = mSigningLineage.get(0).signingCert;
+ if (otherLineage.isCertificateInLineage(signerCert)) {
+ descendantLineage = this;
+ ancestorLineage = otherLineage;
+ } else {
+ descendantLineage = otherLineage;
+ ancestorLineage = this;
+ }
+
+ int ancestorIndex = 0;
+ int descendantIndex = 0;
+ SigningCertificateNode ancestorNode;
+ SigningCertificateNode descendantNode = descendantLineage.mSigningLineage.get(
+ descendantIndex++);
+ List<SigningCertificateNode> mergedLineage = new ArrayList<>();
+ // Iterate through the ancestor lineage and add the current node to the resulting lineage
+ // until the first node of the descendant is found.
+ while (ancestorIndex < ancestorLineage.size()) {
+ ancestorNode = ancestorLineage.mSigningLineage.get(ancestorIndex++);
+ if (ancestorNode.signingCert.equals(descendantNode.signingCert)) {
+ break;
+ }
+ mergedLineage.add(ancestorNode);
+ }
+ // If all of the nodes in the ancestor lineage have been added to the merged lineage, then
+ // there is no overlap between this and the provided lineage.
+ if (ancestorIndex == mergedLineage.size()) {
+ throw new IllegalArgumentException(
+ "The provided lineage is not a descendant or an ancestor of this lineage");
+ }
+ // The descendant lineage's first node was in the ancestor's lineage above; add it to the
+ // merged lineage.
+ mergedLineage.add(descendantNode);
+ while (ancestorIndex < ancestorLineage.size()
+ && descendantIndex < descendantLineage.size()) {
+ ancestorNode = ancestorLineage.mSigningLineage.get(ancestorIndex++);
+ descendantNode = descendantLineage.mSigningLineage.get(descendantIndex++);
+ if (!ancestorNode.signingCert.equals(descendantNode.signingCert)) {
+ throw new IllegalArgumentException(
+ "The provided lineage diverges from this lineage");
+ }
+ mergedLineage.add(descendantNode);
+ }
+ // At this point, one or both of the lineages have been exhausted and all signers to this
+ // point were a match between the two lineages; add any remaining elements from either
+ // lineage to the merged lineage.
+ while (ancestorIndex < ancestorLineage.size()) {
+ mergedLineage.add(ancestorLineage.mSigningLineage.get(ancestorIndex++));
+ }
+ while (descendantIndex < descendantLineage.size()) {
+ mergedLineage.add(descendantLineage.mSigningLineage.get(descendantIndex++));
+ }
+ return new SigningCertificateLineage(Math.min(mMinSdkVersion, otherLineage.mMinSdkVersion),
+ mergedLineage);
+ }
+
+ /**
+ * Checks whether given lineages are compatible. Returns {@code true} if an installed APK with
+ * the oldLineage could be updated with an APK with the newLineage.
+ */
+ public static boolean checkLineagesCompatibility(
+ SigningCertificateLineage oldLineage, SigningCertificateLineage newLineage) {
+
+ final ArrayList<X509Certificate> oldCertificates = oldLineage == null ?
+ new ArrayList<X509Certificate>()
+ : new ArrayList(oldLineage.getCertificatesInLineage());
+ final ArrayList<X509Certificate> newCertificates = newLineage == null ?
+ new ArrayList<X509Certificate>()
+ : new ArrayList(newLineage.getCertificatesInLineage());
+
+ if (oldCertificates.isEmpty()) {
+ return true;
+ }
+ if (newCertificates.isEmpty()) {
+ return false;
+ }
+
+ // Both lineages contain exactly the same certificates or the new lineage extends
+ // the old one. The capabilities of particular certificates may have changed though but it
+ // does not matter in terms of current compatibility.
+ if (newCertificates.size() >= oldCertificates.size()
+ && newCertificates.subList(0, oldCertificates.size()).equals(oldCertificates)) {
+ return true;
+ }
+
+ ArrayList<X509Certificate> newCertificatesArray = new ArrayList(newCertificates);
+ ArrayList<X509Certificate> oldCertificatesArray = new ArrayList(oldCertificates);
+
+ int lastOldCertIndexInNew = newCertificatesArray.lastIndexOf(
+ oldCertificatesArray.get(oldCertificatesArray.size()-1));
+
+ // The new lineage trims some nodes from the beginning of the old lineage and possibly
+ // extends it at the end. The new lineage must contain the old signing certificate and
+ // the nodes up until the node with signing certificate must be in the same order.
+ // Good example 1:
+ // old: A -> B -> C
+ // new: B -> C -> D
+ // Good example 2:
+ // old: A -> B -> C
+ // new: C
+ // Bad example 1:
+ // old: A -> B -> C
+ // new: A -> C
+ // Bad example 1:
+ // old: A -> B
+ // new: C -> B
+ if (lastOldCertIndexInNew >= 0) {
+ return newCertificatesArray.subList(0, lastOldCertIndexInNew+1).equals(
+ oldCertificatesArray.subList(
+ oldCertificates.size()-1-lastOldCertIndexInNew,
+ oldCertificatesArray.size()));
+ }
+
+
+ // The new lineage can be shorter than the old one only if the last certificate of the new
+ // lineage exists in the old lineage and has a rollback capability there.
+ // Good example:
+ // old: A -> B_withRollbackCapability -> C
+ // new: A -> B
+ // Bad example 1:
+ // old: A -> B -> C
+ // new: A -> B
+ // Bad example 2:
+ // old: A -> B_withRollbackCapability -> C
+ // new: A -> B -> D
+ return oldCertificates.subList(0, newCertificates.size()).equals(newCertificates)
+ && oldLineage.getSignerCapabilities(
+ oldCertificates.get(newCertificates.size()-1)).hasRollback();
+ }
+
+ /**
+ * 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.
+ *
+ * <p>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.
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (!(other instanceof SignerCapabilities)) return false;
+
+ return this.mFlags == ((SignerCapabilities) other).mFlags;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * 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.
+ *
+ * <note> 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
+ * </note>
+ */
+ 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}
+ *
+ * <p>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;
+ }
+
+ /**
+ * Constructs a new {@code Builder} that is intended to create a {@code
+ * SigningCertificateLineage} with a single signer in the signing history.
+ *
+ * @param originalSignerConfig first signer in this lineage
+ */
+ public Builder(SignerConfig originalSignerConfig) {
+ if (originalSignerConfig == null) {
+ throw new NullPointerException("Can't pass null SignerConfigs when constructing a "
+ + "new SigningCertificateLineage");
+ }
+ mOriginalSignerConfig = originalSignerConfig;
+ mNewSignerConfig = null;
+ }
+
+ /**
+ * 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.
+ *
+ * <note> 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. </note>
+ */
+ 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 (mNewSignerConfig == null) {
+ return createSigningLineage(mMinSdkVersion, mOriginalSignerConfig,
+ mOriginalCapabilities);
+ }
+
+ if (mNewCapabilities == null) {
+ mNewCapabilities = new SignerCapabilities.Builder().build();
+ }
+
+ return createSigningLineage(
+ mMinSdkVersion, mOriginalSignerConfig, mOriginalCapabilities,
+ mNewSignerConfig, mNewCapabilities);
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/SourceStampVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/SourceStampVerifier.java
new file mode 100644
index 0000000000..98da68ea8f
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/SourceStampVerifier.java
@@ -0,0 +1,911 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig;
+
+import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtilsLite;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.SignatureNotFoundException;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
+import com.android.apksig.internal.apk.v2.V2SchemeConstants;
+import com.android.apksig.internal.apk.v3.V3SchemeConstants;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.DataSources;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APK source stamp verifier intended only to verify the validity of the stamp signature.
+ *
+ * <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
+ * when obtaining the digests for verification. This verifier should only be used in cases where
+ * another mechanism has already been used to verify the APK signatures.
+ */
+public class SourceStampVerifier {
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+
+ private SourceStampVerifier(
+ File apkFile,
+ DataSource apkDataSource,
+ int minSdkVersion,
+ int maxSdkVersion) {
+ mApkFile = apkFile;
+ mApkDataSource = apkDataSource;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ }
+
+ /**
+ * Verifies the APK's source stamp signature and returns the result of the verification.
+ *
+ * <p>The APK's source stamp can be considered verified if the result's {@link
+ * Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
+ * resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
+ * can be obtained as follows:
+ * <ul>
+ * <li>Obtain the generic errors via {@link Result#getErrors()}
+ * <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
+ * query for any errors with {@link Result.SignerInfo#getErrors()}
+ * <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
+ * query for any errors with {@link Result.SignerInfo#getErrors()}
+ * <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
+ * for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
+ * </ul>
+ */
+ public SourceStampVerifier.Result verifySourceStamp() {
+ return verifySourceStamp(null);
+ }
+
+ /**
+ * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
+ * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
+ * of the verification.
+ *
+ * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
+ * if present, without verifying the actual source stamp certificate used to sign the source
+ * stamp. This can be used to verify an APK contains a properly signed source stamp without
+ * verifying a particular signer.
+ *
+ * @see #verifySourceStamp()
+ */
+ public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
+ Closeable in = null;
+ try {
+ DataSource apk;
+ if (mApkDataSource != null) {
+ apk = mApkDataSource;
+ } else if (mApkFile != null) {
+ RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
+ in = f;
+ apk = DataSources.asDataSource(f, 0, f.length());
+ } else {
+ throw new IllegalStateException("APK not provided");
+ }
+ return verifySourceStamp(apk, expectedCertDigest);
+ } catch (IOException e) {
+ Result result = new Result();
+ result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
+ return result;
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Verifies the provided {@code apk}'s source stamp signature, including verification of the
+ * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
+ * returns the result of the verification.
+ *
+ * @see #verifySourceStamp(String)
+ */
+ private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
+ String expectedCertDigest) {
+ Result result = new Result();
+ try {
+ ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
+ // Attempt to obtain the source stamp's certificate digest from the APK.
+ List<CentralDirectoryRecord> cdRecords =
+ ZipUtils.parseZipCentralDirectory(apk, zipSections);
+ CentralDirectoryRecord sourceStampCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+ sourceStampCdRecord = cdRecord;
+ break;
+ }
+ }
+
+ // If the source stamp's certificate digest is not available within the APK then the
+ // source stamp cannot be verified; check if a source stamp signing block is in the
+ // APK's signature block to determine the appropriate status to return.
+ if (sourceStampCdRecord == null) {
+ boolean stampSigningBlockFound;
+ try {
+ ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
+ stampSigningBlockFound = true;
+ } catch (SignatureNotFoundException e) {
+ stampSigningBlockFound = false;
+ }
+ result.addVerificationError(stampSigningBlockFound
+ ? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
+ : ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
+ return result;
+ }
+
+ // Verify that the contents of the source stamp certificate digest match the expected
+ // value, if provided.
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ if (expectedCertDigest != null) {
+ String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
+ sourceStampCertificateDigest);
+ if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
+ result.addVerificationError(
+ ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
+ actualCertDigest, expectedCertDigest);
+ return result;
+ }
+ }
+
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
+ new HashMap<>();
+ if (mMaxSdkVersion >= AndroidSdkVersion.P) {
+ SignatureInfo signatureInfo;
+ try {
+ signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ } catch (SignatureNotFoundException e) {
+ signatureInfo = null;
+ }
+ if (signatureInfo != null) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
+ apkContentDigests, result);
+ signatureSchemeApkContentDigests.put(
+ VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
+ }
+ }
+
+ if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
+ signatureSchemeApkContentDigests.isEmpty())) {
+ SignatureInfo signatureInfo;
+ try {
+ signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+ } catch (SignatureNotFoundException e) {
+ signatureInfo = null;
+ }
+ if (signatureInfo != null) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
+ apkContentDigests, result);
+ signatureSchemeApkContentDigests.put(
+ VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
+ }
+ }
+
+ if (mMinSdkVersion < AndroidSdkVersion.N
+ || signatureSchemeApkContentDigests.isEmpty()) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
+ signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
+ apkContentDigests);
+ }
+
+ ApkSigResult sourceStampResult =
+ V2SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ mMinSdkVersion,
+ mMaxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ return result;
+ } catch (ApkFormatException | IOException | ZipFormatException e) {
+ result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
+ } catch (NoSuchAlgorithmException e) {
+ result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
+ } catch (SignatureNotFoundException e) {
+ result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
+ }
+ return result;
+ }
+
+ /**
+ * Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
+ * {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the
+ * {@code [minSdkVersion, maxSdkVersion]} range.
+ */
+ public static void parseSigners(
+ ByteBuffer apkSignatureSchemeBlock,
+ int apkSigSchemeVersion,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ Result result) {
+ boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
+ // Both the V2 and V3 signature blocks contain the following:
+ // * length-prefixed sequence of length-prefixed signers
+ ByteBuffer signers;
+ try {
+ signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
+ } catch (ApkFormatException e) {
+ result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
+ : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
+ return;
+ }
+ if (!signers.hasRemaining()) {
+ result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
+ : ApkVerificationIssue.V3_SIG_NO_SIGNERS);
+ return;
+ }
+
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+ while (signers.hasRemaining()) {
+ Result.SignerInfo signerInfo = new Result.SignerInfo();
+ if (isV2Block) {
+ result.addV2Signer(signerInfo);
+ } else {
+ result.addV3Signer(signerInfo);
+ }
+ try {
+ ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
+ parseSigner(
+ signer,
+ apkSigSchemeVersion,
+ certFactory,
+ apkContentDigests,
+ signerInfo);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addVerificationWarning(
+ isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
+ : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Parses the provided signer block and populates the {@code result}.
+ *
+ * <p>This verifies signatures over {@code signed-data} contained in this block but does not
+ * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
+ * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
+ * integrity of the APK.
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the
+ * {@code [minSdkVersion, maxSdkVersion]} range.
+ */
+ private static void parseSigner(
+ ByteBuffer signerBlock,
+ int apkSigSchemeVersion,
+ CertificateFactory certFactory,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ Result.SignerInfo signerInfo)
+ throws ApkFormatException {
+ boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
+ // Both the V2 and V3 signer blocks contain the following:
+ // * length-prefixed signed data
+ // * length-prefixed sequence of length-prefixed digests:
+ // * uint32: signature algorithm ID
+ // * length-prefixed bytes: digest of contents
+ // * length-prefixed sequence of certificates:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+ ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock);
+ ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
+ ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
+
+ // Parse the digests block
+ while (digests.hasRemaining()) {
+ try {
+ ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests);
+ int sigAlgorithmId = digest.getInt();
+ byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest);
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addVerificationWarning(
+ isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
+ : ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
+ return;
+ }
+ }
+
+ // Parse the certificates block
+ if (certificates.hasRemaining()) {
+ byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates);
+ X509Certificate certificate;
+ try {
+ certificate = (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(encodedCert));
+ } catch (CertificateException e) {
+ signerInfo.addVerificationWarning(
+ isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
+ : ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
+ return;
+ }
+ // Wrap the cert so that the result's getEncoded returns exactly the original encoded
+ // form. Without this, getEncoded may return a different form from what was stored in
+ // the signature. This is because some X509Certificate(Factory) implementations
+ // re-encode certificates.
+ certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
+ signerInfo.setSigningCertificate(certificate);
+ }
+
+ if (signerInfo.getSigningCertificate() == null) {
+ signerInfo.addVerificationWarning(
+ isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
+ : ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
+ return;
+ }
+ }
+
+ /**
+ * Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
+ * V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
+ * returned.
+ *
+ * <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
+ * will be updated to include a warning, but the source stamp verification can still proceed.
+ */
+ private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
+ List<CentralDirectoryRecord> cdRecords,
+ DataSource apk,
+ ZipSections zipSections,
+ Result result)
+ throws IOException, ApkFormatException {
+ CentralDirectoryRecord manifestCdRecord = null;
+ List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
+ Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
+ ContentDigestAlgorithm.class);
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ String cdRecordName = cdRecord.getName();
+ if (cdRecordName == null) {
+ continue;
+ }
+ if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
+ manifestCdRecord = cdRecord;
+ continue;
+ }
+ if (cdRecordName.startsWith("META-INF/")
+ && (cdRecordName.endsWith(".RSA")
+ || cdRecordName.endsWith(".DSA")
+ || cdRecordName.endsWith(".EC"))) {
+ signatureBlockRecords.add(cdRecord);
+ }
+ }
+ if (manifestCdRecord == null) {
+ // No JAR signing manifest file found. For SourceStamp verification, returning an empty
+ // digest is enough since this would affect the final digest signed by the stamp, and
+ // thus an empty digest will invalidate that signature.
+ return v1ContentDigest;
+ }
+ if (signatureBlockRecords.isEmpty()) {
+ result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
+ } else {
+ for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
+ signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
+ for (Certificate certificate : certFactory.generateCertificates(
+ new ByteArrayInputStream(signatureBlockBytes))) {
+ // If multiple certificates are found within the signature block only the
+ // first is used as the signer of this block.
+ if (certificate instanceof X509Certificate) {
+ Result.SignerInfo signerInfo = new Result.SignerInfo();
+ signerInfo.setSigningCertificate((X509Certificate) certificate);
+ result.addV1Signer(signerInfo);
+ break;
+ }
+ }
+ } catch (CertificateException e) {
+ // Log a warning for the parsing exception but still proceed with the stamp
+ // verification.
+ result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+ signatureBlockRecord.getName(), e);
+ break;
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ }
+ }
+ try {
+ byte[] manifestBytes =
+ LocalFileRecord.getUncompressedData(
+ apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
+ v1ContentDigest.put(
+ ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
+ return v1ContentDigest;
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ }
+
+ /**
+ * Result of verifying the APK's source stamp signature; this signature can only be considered
+ * verified if {@link #isVerified()} returns true.
+ */
+ public static class Result {
+ private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
+ private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
+ private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
+ private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
+ mV2SchemeSigners, mV3SchemeSigners);
+ private SourceStampInfo mSourceStampInfo;
+
+ private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+ private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+
+ private boolean mVerified;
+
+ void addVerificationError(int errorId, Object... params) {
+ mErrors.add(new ApkVerificationIssue(errorId, params));
+ }
+
+ void addVerificationWarning(int warningId, Object... params) {
+ mWarnings.add(new ApkVerificationIssue(warningId, params));
+ }
+
+ private void addV1Signer(SignerInfo signerInfo) {
+ mV1SchemeSigners.add(signerInfo);
+ }
+
+ private void addV2Signer(SignerInfo signerInfo) {
+ mV2SchemeSigners.add(signerInfo);
+ }
+
+ private void addV3Signer(SignerInfo signerInfo) {
+ mV3SchemeSigners.add(signerInfo);
+ }
+
+ /**
+ * Returns {@code true} if the APK's source stamp signature
+ */
+ public boolean isVerified() {
+ return mVerified;
+ }
+
+ private void mergeFrom(ApkSigResult source) {
+ switch (source.signatureSchemeVersion) {
+ case Constants.VERSION_SOURCE_STAMP:
+ mVerified = source.verified;
+ if (!source.mSigners.isEmpty()) {
+ mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
+ }
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown ApkSigResult Signing Block Scheme Id "
+ + source.signatureSchemeVersion);
+ }
+ }
+
+ /**
+ * Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
+ * provided APK.
+ */
+ public List<SignerInfo> getV1SchemeSigners() {
+ return mV1SchemeSigners;
+ }
+
+ /**
+ * Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
+ * provided APK.
+ */
+ public List<SignerInfo> getV2SchemeSigners() {
+ return mV2SchemeSigners;
+ }
+
+ /**
+ * Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the
+ * provided APK.
+ */
+ public List<SignerInfo> getV3SchemeSigners() {
+ return mV3SchemeSigners;
+ }
+
+ /**
+ * Returns the {@link SourceStampInfo} instance representing the source stamp signer for the
+ * APK, or null if the source stamp signature verification failed before the stamp signature
+ * block could be fully parsed.
+ */
+ public SourceStampInfo getSourceStampInfo() {
+ return mSourceStampInfo;
+ }
+
+ /**
+ * Returns {@code true} if an error was encountered while verifying the APK.
+ *
+ * <p>Any error prevents the APK from being considered verified.
+ */
+ public boolean containsErrors() {
+ if (!mErrors.isEmpty()) {
+ return true;
+ }
+ for (List<SignerInfo> signers : mAllSchemeSigners) {
+ for (SignerInfo signer : signers) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+ if (mSourceStampInfo != null) {
+ if (mSourceStampInfo.containsErrors()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the errors encountered while verifying the APK's source stamp.
+ */
+ public List<ApkVerificationIssue> getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns the warnings encountered while verifying the APK's source stamp.
+ */
+ public List<ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+
+ /**
+ * Returns all errors for this result, including any errors from signature scheme signers
+ * and the source stamp.
+ */
+ public List<ApkVerificationIssue> getAllErrors() {
+ List<ApkVerificationIssue> errors = new ArrayList<>();
+ errors.addAll(mErrors);
+
+ for (List<SignerInfo> signers : mAllSchemeSigners) {
+ for (SignerInfo signer : signers) {
+ errors.addAll(signer.getErrors());
+ }
+ }
+ if (mSourceStampInfo != null) {
+ errors.addAll(mSourceStampInfo.getErrors());
+ }
+ return errors;
+ }
+
+ /**
+ * Returns all warnings for this result, including any warnings from signature scheme
+ * signers and the source stamp.
+ */
+ public List<ApkVerificationIssue> getAllWarnings() {
+ List<ApkVerificationIssue> warnings = new ArrayList<>();
+ warnings.addAll(mWarnings);
+
+ for (List<SignerInfo> signers : mAllSchemeSigners) {
+ for (SignerInfo signer : signers) {
+ warnings.addAll(signer.getWarnings());
+ }
+ }
+ if (mSourceStampInfo != null) {
+ warnings.addAll(mSourceStampInfo.getWarnings());
+ }
+ return warnings;
+ }
+
+ /**
+ * Contains information about an APK's signer and any errors encountered while parsing the
+ * corresponding signature block.
+ */
+ public static class SignerInfo {
+ private X509Certificate mSigningCertificate;
+ private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+ private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+
+ void setSigningCertificate(X509Certificate signingCertificate) {
+ mSigningCertificate = signingCertificate;
+ }
+
+ void addVerificationError(int errorId, Object... params) {
+ mErrors.add(new ApkVerificationIssue(errorId, params));
+ }
+
+ void addVerificationWarning(int warningId, Object... params) {
+ mWarnings.add(new ApkVerificationIssue(warningId, params));
+ }
+
+ /**
+ * Returns the current signing certificate used by this signer.
+ */
+ public X509Certificate getSigningCertificate() {
+ return mSigningCertificate;
+ }
+
+ /**
+ * Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors
+ * encountered during processing of this signer's signature block.
+ */
+ public List<ApkVerificationIssue> getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
+ * encountered during processing of this signer's signature block.
+ */
+ public List<ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+
+ /**
+ * Returns {@code true} if any errors were encountered while parsing this signer's
+ * signature block.
+ */
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+ }
+
+ /**
+ * Contains information about an APK's source stamp and any errors encountered while
+ * parsing the stamp signature block.
+ */
+ public static class SourceStampInfo {
+ private final List<X509Certificate> mCertificates;
+ private final List<X509Certificate> mCertificateLineage;
+
+ private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+ private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+ private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
+
+ private final long mTimestamp;
+
+ /*
+ * Since this utility is intended just to verify the source stamp, and the source stamp
+ * currently only logs warnings to prevent failing the APK signature verification, treat
+ * all warnings as errors. If the stamp verification is updated to log errors this
+ * should be set to false to ensure only errors trigger a failure verifying the source
+ * stamp.
+ */
+ private static final boolean mWarningsAsErrors = true;
+
+ private SourceStampInfo(ApkSignerInfo result) {
+ mCertificates = result.certs;
+ mCertificateLineage = result.certificateLineage;
+ mErrors.addAll(result.getErrors());
+ mWarnings.addAll(result.getWarnings());
+ mInfoMessages.addAll(result.getInfoMessages());
+ mTimestamp = result.timestamp;
+ }
+
+ /**
+ * Returns the SourceStamp's signing certificate or {@code null} if not available. The
+ * certificate is guaranteed to be available if no errors were encountered during
+ * verification (see {@link #containsErrors()}.
+ *
+ * <p>This certificate contains the SourceStamp's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCertificates.isEmpty() ? null : mCertificates.get(0);
+ }
+
+ /**
+ * Returns a {@code List} of {@link X509Certificate} instances representing the source
+ * stamp signer's lineage with the oldest signer at element 0, or an empty {@code List}
+ * if the stamp's signing certificate has not been rotated.
+ */
+ public List<X509Certificate> getCertificatesInLineage() {
+ return mCertificateLineage;
+ }
+
+ /**
+ * Returns whether any errors were encountered during the source stamp verification.
+ */
+ public boolean containsErrors() {
+ return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty());
+ }
+
+ /**
+ * Returns {@code true} if any info messages were encountered during verification of
+ * this source stamp.
+ */
+ public boolean containsInfoMessages() {
+ return !mInfoMessages.isEmpty();
+ }
+
+ /**
+ * Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were
+ * encountered during source stamp verification.
+ */
+ public List<ApkVerificationIssue> getErrors() {
+ if (!mWarningsAsErrors) {
+ return mErrors;
+ }
+ List<ApkVerificationIssue> result = new ArrayList<>();
+ result.addAll(mErrors);
+ result.addAll(mWarnings);
+ return result;
+ }
+
+ /**
+ * Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that
+ * were encountered during source stamp verification.
+ */
+ public List<ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+
+ /**
+ * Returns a {@code List} of {@link ApkVerificationIssue} representing info messages
+ * that were encountered during source stamp verification.
+ */
+ public List<ApkVerificationIssue> getInfoMessages() {
+ return mInfoMessages;
+ }
+
+ /**
+ * Returns the epoch timestamp in seconds representing the time this source stamp block
+ * was signed, or 0 if the timestamp is not available.
+ */
+ public long getTimestampEpochSeconds() {
+ return mTimestamp;
+ }
+ }
+ }
+
+ /**
+ * Builder of {@link SourceStampVerifier} instances.
+ *
+ * <p> The resulting verifier, by default, checks whether the APK's source stamp signature will
+ * verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not
+ * queried to determine the APK's minimum supported level, so the caller should specify a lower
+ * bound with {@link #setMinCheckedPlatformVersion(int)}.
+ */
+ public static class Builder {
+ private final File mApkFile;
+ private final DataSource mApkDataSource;
+
+ private int mMinSdkVersion = 1;
+ private int mMaxSdkVersion = Integer.MAX_VALUE;
+
+ /**
+ * Constructs a new {@code Builder} for source stamp verification of the provided {@code
+ * apk}.
+ */
+ public Builder(File apk) {
+ if (apk == null) {
+ throw new NullPointerException("apk == null");
+ }
+ mApkFile = apk;
+ mApkDataSource = null;
+ }
+
+ /**
+ * Constructs a new {@code Builder} for source stamp verification of the provided {@code
+ * apk}.
+ */
+ public Builder(DataSource apk) {
+ if (apk == null) {
+ throw new NullPointerException("apk == null");
+ }
+ mApkDataSource = apk;
+ mApkFile = null;
+ }
+
+ /**
+ * Sets the oldest Android platform version for which the APK's source stamp is verified.
+ *
+ * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
+ * on all Android platforms starting from the platform version with the provided {@code
+ * minSdkVersion}. The upper end of the platform versions range can be modified via
+ * {@link #setMaxCheckedPlatformVersion(int)}.
+ *
+ * @param minSdkVersion API Level of the oldest platform for which to verify the APK
+ */
+ public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) {
+ mMinSdkVersion = minSdkVersion;
+ return this;
+ }
+
+ /**
+ * Sets the newest Android platform version for which the APK's source stamp is verified.
+ *
+ * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
+ * on all platform versions up to and including the proviced {@code maxSdkVersion}. The
+ * lower end of the platform versions range can be modified via {@link
+ * #setMinCheckedPlatformVersion(int)}.
+ *
+ * @param maxSdkVersion API Level of the newest platform for which to verify the APK
+ * @see #setMinCheckedPlatformVersion(int)
+ */
+ public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
+ mMaxSdkVersion = maxSdkVersion;
+ return this;
+ }
+
+ /**
+ * Returns a {@link SourceStampVerifier} initialized according to the configuration of this
+ * builder.
+ */
+ public SourceStampVerifier build() {
+ return new SourceStampVerifier(
+ mApkFile,
+ mApkDataSource,
+ mMinSdkVersion,
+ mMaxSdkVersion);
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkFormatException.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkFormatException.java
new file mode 100644
index 0000000000..a780134498
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java
new file mode 100644
index 0000000000..fd961d5716
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtils.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtils.java
new file mode 100644
index 0000000000..156ea17c00
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -0,0 +1,670 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.apk;
+
+import com.android.apksig.internal.apk.AndroidBinXmlParser;
+import com.android.apksig.internal.apk.stamp.SourceStampConstants;
+import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * APK utilities.
+ */
+public abstract class ApkUtils {
+
+ /**
+ * Name of the Android manifest ZIP entry in APKs.
+ */
+ public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
+
+ /** Name of the SourceStamp certificate hash ZIP entry in APKs. */
+ public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME =
+ SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+
+ private ApkUtils() {}
+
+ /**
+ * Finds the main ZIP sections of the provided APK.
+ *
+ * @throws IOException if an I/O error occurred while reading the APK
+ * @throws ZipFormatException if the APK is malformed
+ */
+ public static ZipSections findZipSections(DataSource apk)
+ throws IOException, ZipFormatException {
+ com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
+ return new ZipSections(
+ zipSections.getZipCentralDirectoryOffset(),
+ zipSections.getZipCentralDirectorySizeBytes(),
+ zipSections.getZipCentralDirectoryRecordCount(),
+ zipSections.getZipEndOfCentralDirectoryOffset(),
+ zipSections.getZipEndOfCentralDirectory());
+ }
+
+ /**
+ * Information about the ZIP sections of an APK.
+ */
+ public static class ZipSections extends com.android.apksig.zip.ZipSections {
+ public ZipSections(
+ long centralDirectoryOffset,
+ long centralDirectorySizeBytes,
+ int centralDirectoryRecordCount,
+ long eocdOffset,
+ ByteBuffer eocd) {
+ super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount,
+ eocdOffset, eocd);
+ }
+ }
+
+ /**
+ * Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
+ * Directory record.
+ *
+ * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
+ * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
+ * be between {@code 0} and {@code 2^32 - 1} inclusive.
+ */
+ public static void setZipEocdCentralDirectoryOffset(
+ ByteBuffer zipEndOfCentralDirectory, long offset) {
+ ByteBuffer eocd = zipEndOfCentralDirectory.slice();
+ eocd.order(ByteOrder.LITTLE_ENDIAN);
+ ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
+ }
+
+ /**
+ * Updates the length of EOCD comment.
+ *
+ * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
+ */
+ public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
+ ByteBuffer eocd = zipEndOfCentralDirectory.slice();
+ eocd.order(ByteOrder.LITTLE_ENDIAN);
+ ZipUtils.updateZipEocdCommentLen(eocd);
+ }
+
+ /**
+ * Returns the APK Signing Block of the provided {@code apk}.
+ *
+ * @throws ApkFormatException if the APK is not a valid ZIP archive
+ * @throws IOException if an I/O error occurs
+ * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+ * </a>
+ */
+ public static ApkSigningBlock findApkSigningBlock(DataSource apk)
+ throws ApkFormatException, IOException, ApkSigningBlockNotFoundException {
+ ApkUtils.ZipSections inputZipSections;
+ try {
+ inputZipSections = ApkUtils.findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
+ }
+ return findApkSigningBlock(apk, inputZipSections);
+ }
+
+ /**
+ * Returns the APK Signing Block of the provided APK.
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+ * </a>
+ */
+ public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
+ throws IOException, ApkSigningBlockNotFoundException {
+ ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
+ zipSections);
+ return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
+ }
+
+ /**
+ * Information about the location of the APK Signing Block inside an APK.
+ */
+ public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
+ /**
+ * Constructs a new {@code ApkSigningBlock}.
+ *
+ * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
+ * Signing Block inside the APK file
+ * @param contents contents of the APK Signing Block
+ */
+ public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
+ super(startOffsetInApk, contents);
+ }
+ }
+
+ /**
+ * Returns the contents of the APK's {@code AndroidManifest.xml}.
+ *
+ * @throws IOException if an I/O error occurs while reading the APK
+ * @throws ApkFormatException if the APK is malformed
+ */
+ public static ByteBuffer getAndroidManifest(DataSource apk)
+ throws IOException, ApkFormatException {
+ ZipSections zipSections;
+ try {
+ zipSections = findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Not a valid ZIP archive", e);
+ }
+ List<CentralDirectoryRecord> cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+ CentralDirectoryRecord androidManifestCdRecord = null;
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
+ androidManifestCdRecord = cdRecord;
+ break;
+ }
+ }
+ if (androidManifestCdRecord == null) {
+ throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
+ }
+ DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset());
+
+ try {
+ return ByteBuffer.wrap(
+ LocalFileRecord.getUncompressedData(
+ lfhSection, androidManifestCdRecord, lfhSection.size()));
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
+ }
+ }
+
+ /**
+ * Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml.
+ */
+ private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c;
+
+ /**
+ * Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml.
+ */
+ private static final int DEBUGGABLE_ATTR_ID = 0x0101000f;
+
+ /**
+ * Android resource ID of the {@code android:targetSandboxVersion} attribute in
+ * AndroidManifest.xml.
+ */
+ private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
+
+ /**
+ * Android resource ID of the {@code android:targetSdkVersion} attribute in
+ * AndroidManifest.xml.
+ */
+ private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
+ private static final String USES_SDK_ELEMENT_TAG = "uses-sdk";
+
+ /**
+ * Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml.
+ */
+ private static final int VERSION_CODE_ATTR_ID = 0x0101021b;
+ private static final String MANIFEST_ELEMENT_TAG = "manifest";
+
+ /**
+ * Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml.
+ */
+ private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576;
+
+ /**
+ * Returns the lowest Android platform version (API Level) supported by an APK with the
+ * provided {@code AndroidManifest.xml}.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ *
+ * @throws MinSdkVersionException if an error occurred while determining the API Level
+ */
+ public static int getMinSdkVersionFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) throws MinSdkVersionException {
+ // IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using
+ // uses-sdk elements which are children of the top-level manifest element. uses-sdk element
+ // declares the minimum supported platform version using the android:minSdkVersion attribute
+ // whose default value is 1.
+ // For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion
+ // is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the
+ // effective minSdkVersion value is the maximum over the encountered minSdkVersion values.
+
+ try {
+ // If no uses-sdk elements are encountered, Android accepts the APK. We treat this
+ // scenario as though the minimum supported API Level is 1.
+ int result = 1;
+
+ AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
+ int eventType = parser.getEventType();
+ while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
+ if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
+ && (parser.getDepth() == 2)
+ && ("uses-sdk".equals(parser.getName()))
+ && (parser.getNamespace().isEmpty())) {
+ // In each uses-sdk element, minSdkVersion defaults to 1
+ int minSdkVersion = 1;
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) {
+ int valueType = parser.getAttributeValueType(i);
+ switch (valueType) {
+ case AndroidBinXmlParser.VALUE_TYPE_INT:
+ minSdkVersion = parser.getAttributeIntValue(i);
+ break;
+ case AndroidBinXmlParser.VALUE_TYPE_STRING:
+ minSdkVersion =
+ getMinSdkVersionForCodename(
+ parser.getAttributeStringValue(i));
+ break;
+ default:
+ throw new MinSdkVersionException(
+ "Unable to determine APK's minimum supported Android"
+ + ": unsupported value type in "
+ + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+ + " minSdkVersion"
+ + ". Only integer values supported.");
+ }
+ break;
+ }
+ }
+ result = Math.max(result, minSdkVersion);
+ }
+ eventType = parser.next();
+ }
+
+ return result;
+ } catch (AndroidBinXmlParser.XmlParserException e) {
+ throw new MinSdkVersionException(
+ "Unable to determine APK's minimum supported Android platform version"
+ + ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
+ e);
+ }
+ }
+
+ private static class CodenamesLazyInitializer {
+
+ /**
+ * List of platform codename (first letter of) to API Level mappings. The list must be
+ * sorted by the first letter. For codenames not in the list, the assumption is that the API
+ * Level is incremented by one for every increase in the codename's first letter.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static final Pair<Character, Integer>[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL =
+ new Pair[] {
+ Pair.of('C', 2),
+ Pair.of('D', 3),
+ Pair.of('E', 4),
+ Pair.of('F', 7),
+ Pair.of('G', 8),
+ Pair.of('H', 10),
+ Pair.of('I', 13),
+ Pair.of('J', 15),
+ Pair.of('K', 18),
+ Pair.of('L', 20),
+ Pair.of('M', 22),
+ Pair.of('N', 23),
+ Pair.of('O', 25),
+ };
+
+ private static final Comparator<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR =
+ new ByFirstComparator();
+
+ private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> {
+ @Override
+ public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) {
+ char c1 = o1.getFirst();
+ char c2 = o2.getFirst();
+ return c1 - c2;
+ }
+ }
+ }
+
+ /**
+ * Returns the API Level corresponding to the provided platform codename.
+ *
+ * <p>This method is pessimistic. It returns a value one lower than the API Level with which the
+ * platform is actually released (e.g., 23 for N which was released as API Level 24). This is
+ * because new features which first appear in an API Level are not available in the early days
+ * of that platform version's existence, when the platform only has a codename. Moreover, this
+ * method currently doesn't differentiate between initial and MR releases, meaning API Level
+ * returned for MR releases may be more than one lower than the API Level with which the
+ * platform version is actually released.
+ *
+ * @throws CodenameMinSdkVersionException if the {@code codename} is not supported
+ */
+ static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException {
+ char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0);
+ // Codenames are case-sensitive. Only codenames starting with A-Z are supported for now.
+ // We only look at the first letter of the codename as this is the most important letter.
+ if ((firstChar >= 'A') && (firstChar <= 'Z')) {
+ Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel =
+ CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL;
+ int searchResult =
+ Arrays.binarySearch(
+ sortedCodenamesFirstCharToApiLevel,
+ Pair.of(firstChar, null), // second element of the pair is ignored here
+ CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR);
+ if (searchResult >= 0) {
+ // Exact match -- searchResult is the index of the matching element
+ return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond();
+ }
+ // Not an exact match -- searchResult is negative and is -(insertion index) - 1.
+ // The element at insertionIndex - 1 (if present) is smaller than firstChar and the
+ // element at insertionIndex (if present) is greater than firstChar.
+ int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length]
+ if (insertionIndex == 0) {
+ // 'A' or 'B' -- never released to public
+ return 1;
+ } else {
+ // The element at insertionIndex - 1 is the newest older codename.
+ // API Level bumped by at least 1 for every change in the first letter of codename
+ Pair<Character, Integer> newestOlderCodenameMapping =
+ sortedCodenamesFirstCharToApiLevel[insertionIndex - 1];
+ char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst();
+ int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond();
+ return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar);
+ }
+ }
+
+ throw new CodenameMinSdkVersionException(
+ "Unable to determine APK's minimum supported Android platform version"
+ + " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME
+ + "'s minSdkVersion: \"" + codename + "\"",
+ codename);
+ }
+
+ /**
+ * Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}.
+ * See the {@code android:debuggable} attribute of the {@code application} element.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ *
+ * @throws ApkFormatException if the manifest is malformed
+ */
+ public static boolean getDebuggableFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) throws ApkFormatException {
+ // IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first
+ // "application" element which is a child of the top-level manifest element. The debuggable
+ // attribute of this application element is coerced to a boolean value. If there is no
+ // application element or if it doesn't declare the debuggable attribute, the package is
+ // considered not debuggable.
+
+ try {
+ AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
+ int eventType = parser.getEventType();
+ while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
+ if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
+ && (parser.getDepth() == 2)
+ && ("application".equals(parser.getName()))
+ && (parser.getNamespace().isEmpty())) {
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) {
+ int valueType = parser.getAttributeValueType(i);
+ switch (valueType) {
+ case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN:
+ case AndroidBinXmlParser.VALUE_TYPE_STRING:
+ case AndroidBinXmlParser.VALUE_TYPE_INT:
+ String value = parser.getAttributeStringValue(i);
+ return ("true".equals(value))
+ || ("TRUE".equals(value))
+ || ("1".equals(value));
+ case AndroidBinXmlParser.VALUE_TYPE_REFERENCE:
+ // References to resources are not supported on purpose. The
+ // reason is that the resolved value depends on the resource
+ // configuration (e.g, MNC/MCC, locale, screen density) used
+ // at resolution time. As a result, the same APK may appear as
+ // debuggable in one situation and as non-debuggable in another
+ // situation. Such APKs may put users at risk.
+ throw new ApkFormatException(
+ "Unable to determine whether APK is debuggable"
+ + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+ + " android:debuggable attribute references a"
+ + " resource. References are not supported for"
+ + " security reasons. Only constant boolean,"
+ + " string and int values are supported.");
+ default:
+ throw new ApkFormatException(
+ "Unable to determine whether APK is debuggable"
+ + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+ + " android:debuggable attribute uses"
+ + " unsupported value type. Only boolean,"
+ + " string and int values are supported.");
+ }
+ }
+ }
+ // This application element does not declare the debuggable attribute
+ return false;
+ }
+ eventType = parser.next();
+ }
+
+ // No application element found
+ return false;
+ } catch (AndroidBinXmlParser.XmlParserException e) {
+ throw new ApkFormatException(
+ "Unable to determine whether APK is debuggable: malformed binary resource: "
+ + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
+ e);
+ }
+ }
+
+ /**
+ * Returns the package name of the APK according to its {@code AndroidManifest.xml} or
+ * {@code null} if package name is not declared. See the {@code package} attribute of the
+ * {@code manifest} element.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ *
+ * @throws ApkFormatException if the manifest is malformed
+ */
+ public static String getPackageNameFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) throws ApkFormatException {
+ // IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level
+ // manifest element. Interestingly, as opposed to most other attributes, Android Package
+ // Manager looks up this attribute by its name rather than by its resource ID.
+
+ try {
+ AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
+ int eventType = parser.getEventType();
+ while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
+ if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
+ && (parser.getDepth() == 1)
+ && ("manifest".equals(parser.getName()))
+ && (parser.getNamespace().isEmpty())) {
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ if ("package".equals(parser.getAttributeName(i))
+ && (parser.getNamespace().isEmpty())) {
+ return parser.getAttributeStringValue(i);
+ }
+ }
+ // No "package" attribute found
+ return null;
+ }
+ eventType = parser.next();
+ }
+
+ // No manifest element found
+ return null;
+ } catch (AndroidBinXmlParser.XmlParserException e) {
+ throw new ApkFormatException(
+ "Unable to determine APK package name: malformed binary resource: "
+ + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
+ e);
+ }
+ }
+
+ /**
+ * Returns the security sandbox version targeted by an APK with the provided
+ * {@code AndroidManifest.xml}.
+ *
+ * <p>If the security sandbox version is not specified in the manifest a default value of 1 is
+ * returned.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ */
+ public static int getTargetSandboxVersionFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) {
+ try {
+ return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID);
+ } catch (ApkFormatException e) {
+ // An ApkFormatException indicates the target sandbox is not specified in the manifest;
+ // return a default value of 1.
+ return 1;
+ }
+ }
+
+ /**
+ * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
+ *
+ * <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither
+ * value is specified then a value of 1 is returned.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ */
+ public static int getTargetSdkVersionFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) {
+ // If the targetSdkVersion is not specified then the platform will use the value of the
+ // minSdkVersion; if neither is specified then the platform will use a value of 1.
+ int minSdkVersion = 1;
+ try {
+ return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID);
+ } catch (ApkFormatException e) {
+ // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
+ // element is not specified at all.
+ }
+ androidManifestContents.rewind();
+ try {
+ minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents);
+ } catch (ApkFormatException e) {
+ // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or
+ // the uses-sdk element is not specified at all.
+ }
+ return minSdkVersion;
+ }
+
+ /**
+ * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}.
+ *
+ * <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid
+ * integer an ApkFormatException is thrown.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ * @throws ApkFormatException if an error occurred while determining the versionCode, or if the
+ * versionCode attribute value is not available.
+ */
+ public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)
+ throws ApkFormatException {
+ return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID);
+ }
+
+ /**
+ * Returns the versionCode and versionCodeMajor of the APK according to its {@code
+ * AndroidManifest.xml} combined together as a single long value.
+ *
+ * <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower
+ * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned.
+ *
+ * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
+ * resource format
+ * @throws ApkFormatException if an error occurred while determining the version, or if the
+ * versionCode attribute value is not available.
+ */
+ public static long getLongVersionCodeFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents) throws ApkFormatException {
+ // If the versionCode is not found then allow the ApkFormatException to be thrown to notify
+ // the caller that the versionCode is not available.
+ int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents);
+ long versionCodeMajor = 0;
+ try {
+ androidManifestContents.rewind();
+ versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
+ MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID);
+ } catch (ApkFormatException e) {
+ // This is expected if the versionCodeMajor has not been defined for the APK; in this
+ // case the return value is just the versionCode.
+ }
+ return (versionCodeMajor << 32) | versionCode;
+ }
+
+ /**
+ * Returns the integer value of the requested {@code attributeId} in the specified {@code
+ * elementName} from the provided {@code androidManifestContents} in binary Android resource
+ * format.
+ *
+ * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or
+ * if the requested attribute is not found.
+ */
+ private static int getAttributeValueFromBinaryAndroidManifest(
+ ByteBuffer androidManifestContents, String elementName, int attributeId)
+ throws ApkFormatException {
+ if (elementName == null) {
+ throw new NullPointerException("elementName cannot be null");
+ }
+ try {
+ AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
+ int eventType = parser.getEventType();
+ while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
+ if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
+ && (elementName.equals(parser.getName()))) {
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ if (parser.getAttributeNameResourceId(i) == attributeId) {
+ int valueType = parser.getAttributeValueType(i);
+ switch (valueType) {
+ case AndroidBinXmlParser.VALUE_TYPE_INT:
+ case AndroidBinXmlParser.VALUE_TYPE_STRING:
+ return parser.getAttributeIntValue(i);
+ default:
+ throw new ApkFormatException(
+ "Unsupported value type, " + valueType
+ + ", for attribute " + String.format("0x%08X",
+ attributeId) + " under element " + elementName);
+
+ }
+ }
+ }
+ }
+ eventType = parser.next();
+ }
+ throw new ApkFormatException(
+ "Failed to determine APK's " + elementName + " attribute "
+ + String.format("0x%08X", attributeId) + " value");
+ } catch (AndroidBinXmlParser.XmlParserException e) {
+ throw new ApkFormatException(
+ "Unable to determine value for attribute " + String.format("0x%08X",
+ attributeId) + " under element " + elementName
+ + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
+ }
+ }
+
+ public static byte[] computeSha256DigestBytes(byte[] data) {
+ return ApkUtilsLite.computeSha256DigestBytes(data);
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtilsLite.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtilsLite.java
new file mode 100644
index 0000000000..13f230119d
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtilsLite.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.apk;
+
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Lightweight version of the ApkUtils for clients that only require a subset of the utility
+ * functionality.
+ */
+public class ApkUtilsLite {
+ private ApkUtilsLite() {}
+
+ /**
+ * Finds the main ZIP sections of the provided APK.
+ *
+ * @throws IOException if an I/O error occurred while reading the APK
+ * @throws ZipFormatException if the APK is malformed
+ */
+ public static ZipSections findZipSections(DataSource apk)
+ throws IOException, ZipFormatException {
+ Pair<ByteBuffer, Long> eocdAndOffsetInFile =
+ ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
+ if (eocdAndOffsetInFile == null) {
+ throw new ZipFormatException("ZIP End of Central Directory record not found");
+ }
+
+ ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
+ long eocdOffset = eocdAndOffsetInFile.getSecond();
+ eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+ long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
+ if (cdStartOffset > eocdOffset) {
+ throw new ZipFormatException(
+ "ZIP Central Directory start offset out of range: " + cdStartOffset
+ + ". ZIP End of Central Directory offset: " + eocdOffset);
+ }
+
+ long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
+ long cdEndOffset = cdStartOffset + cdSizeBytes;
+ if (cdEndOffset > eocdOffset) {
+ throw new ZipFormatException(
+ "ZIP Central Directory overlaps with End of Central Directory"
+ + ". CD end: " + cdEndOffset
+ + ", EoCD start: " + eocdOffset);
+ }
+
+ int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
+
+ return new ZipSections(
+ cdStartOffset,
+ cdSizeBytes,
+ cdRecordCount,
+ eocdOffset,
+ eocdBuf);
+ }
+
+ // See https://source.android.com/security/apksigning/v2.html
+ private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
+ private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
+ private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
+
+ /**
+ * Returns the APK Signing Block of the provided APK.
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
+ * </a>
+ */
+ public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
+ throws IOException, ApkSigningBlockNotFoundException {
+ // FORMAT (see https://source.android.com/security/apksigning/v2.html):
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint64: size in bytes (excluding this field)
+ // * @+8 bytes payload
+ // * @-24 bytes uint64: size in bytes (same as the one above)
+ // * @-16 bytes uint128: magic
+
+ long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
+ long centralDirEndOffset =
+ centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
+ long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
+ if (centralDirEndOffset != eocdStartOffset) {
+ throw new ApkSigningBlockNotFoundException(
+ "ZIP Central Directory is not immediately followed by End of Central Directory"
+ + ". CD end: " + centralDirEndOffset
+ + ", EoCD start: " + eocdStartOffset);
+ }
+
+ if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
+ throw new ApkSigningBlockNotFoundException(
+ "APK too small for APK Signing Block. ZIP Central Directory offset: "
+ + centralDirStartOffset);
+ }
+ // Read the magic and offset in file from the footer section of the block:
+ // * uint64: size of block
+ // * 16 bytes: magic
+ ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
+ footer.order(ByteOrder.LITTLE_ENDIAN);
+ if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
+ || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
+ throw new ApkSigningBlockNotFoundException(
+ "No APK Signing Block before ZIP Central Directory");
+ }
+ // Read and compare size fields
+ long apkSigBlockSizeInFooter = footer.getLong(0);
+ if ((apkSigBlockSizeInFooter < footer.capacity())
+ || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
+ throw new ApkSigningBlockNotFoundException(
+ "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
+ }
+ int totalSize = (int) (apkSigBlockSizeInFooter + 8);
+ long apkSigBlockOffset = centralDirStartOffset - totalSize;
+ if (apkSigBlockOffset < 0) {
+ throw new ApkSigningBlockNotFoundException(
+ "APK Signing Block offset out of range: " + apkSigBlockOffset);
+ }
+ ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
+ apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
+ long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
+ if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
+ throw new ApkSigningBlockNotFoundException(
+ "APK Signing Block sizes in header and footer do not match: "
+ + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
+ }
+ return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
+ }
+
+ /**
+ * Information about the location of the APK Signing Block inside an APK.
+ */
+ public static class ApkSigningBlock {
+ private final long mStartOffsetInApk;
+ private final DataSource mContents;
+
+ /**
+ * Constructs a new {@code ApkSigningBlock}.
+ *
+ * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
+ * Signing Block inside the APK file
+ * @param contents contents of the APK Signing Block
+ */
+ public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
+ mStartOffsetInApk = startOffsetInApk;
+ mContents = contents;
+ }
+
+ /**
+ * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
+ */
+ public long getStartOffset() {
+ return mStartOffsetInApk;
+ }
+
+ /**
+ * Returns the data source which provides the full contents of the APK Signing Block,
+ * including its footer.
+ */
+ public DataSource getContents() {
+ return mContents;
+ }
+ }
+
+ public static byte[] computeSha256DigestBytes(byte[] data) {
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SHA-256 is not found", e);
+ }
+ messageDigest.update(data);
+ return messageDigest.digest();
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java
new file mode 100644
index 0000000000..e30bc359a6
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/apk/MinSdkVersionException.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/MinSdkVersionException.java
new file mode 100644
index 0000000000..c4aad08067
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java
new file mode 100644
index 0000000000..bc5a45738b
--- /dev/null
+++ b/platform/android/java/editor/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}.
+ *
+ * <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
+ * {@link #getEventType()} and {@link #next()} methods. Additional information about the current
+ * event can be obtained via an assortment of getters, for example, {@link #getName()} or
+ * {@link #getAttributeNameResourceId(int)}.
+ */
+public class AndroidBinXmlParser {
+
+ /** Event: start of document. */
+ public static final int EVENT_START_DOCUMENT = 1;
+
+ /** Event: end of document. */
+ public static final int EVENT_END_DOCUMENT = 2;
+
+ /** Event: start of an element. */
+ public static final int EVENT_START_ELEMENT = 3;
+
+ /** Event: end of an document. */
+ public static final int EVENT_END_ELEMENT = 4;
+
+ /** Attribute value type is not supported by this parser. */
+ public static final int VALUE_TYPE_UNSUPPORTED = 0;
+
+ /** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
+ public static final int VALUE_TYPE_STRING = 1;
+
+ /** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
+ public static final int VALUE_TYPE_INT = 2;
+
+ /**
+ * Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
+ */
+ public static final int VALUE_TYPE_REFERENCE = 3;
+
+ /** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
+ public static final int VALUE_TYPE_BOOLEAN = 4;
+
+ private static final long NO_NAMESPACE = 0xffffffffL;
+
+ private final ByteBuffer mXml;
+
+ private StringPool mStringPool;
+ private ResourceMap mResourceMap;
+ private int mDepth;
+ private int mCurrentEvent = EVENT_START_DOCUMENT;
+
+ private String mCurrentElementName;
+ private String mCurrentElementNamespace;
+ private int mCurrentElementAttributeCount;
+ private List<Attribute> mCurrentElementAttributes;
+ private ByteBuffer mCurrentElementAttributesContents;
+ private int mCurrentElementAttrSizeBytes;
+
+ /**
+ * Constructs a new parser for the provided document.
+ */
+ public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
+ xml.order(ByteOrder.LITTLE_ENDIAN);
+
+ Chunk resXmlChunk = null;
+ while (xml.hasRemaining()) {
+ Chunk chunk = Chunk.get(xml);
+ if (chunk == null) {
+ break;
+ }
+ if (chunk.getType() == Chunk.TYPE_RES_XML) {
+ resXmlChunk = chunk;
+ break;
+ }
+ }
+
+ if (resXmlChunk == null) {
+ throw new XmlParserException("No XML chunk in file");
+ }
+ mXml = resXmlChunk.getContents();
+ }
+
+ /**
+ * Returns the depth of the current element. Outside of the root of the document the depth is
+ * {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
+ * is decremented by {@code 1} after each {@code end element} event.
+ */
+ public int getDepth() {
+ return mDepth;
+ }
+
+ /**
+ * Returns the type of the current event. See {@code EVENT_...} constants.
+ */
+ public int getEventType() {
+ return mCurrentEvent;
+ }
+
+ /**
+ * Returns the local name of the current element or {@code null} if the current event does not
+ * pertain to an element.
+ */
+ public String getName() {
+ if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
+ return null;
+ }
+ return mCurrentElementName;
+ }
+
+ /**
+ * Returns the namespace of the current element or {@code null} if the current event does not
+ * pertain to an element. Returns an empty string if the element is not associated with a
+ * namespace.
+ */
+ public String getNamespace() {
+ if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
+ return null;
+ }
+ return mCurrentElementNamespace;
+ }
+
+ /**
+ * Returns the number of attributes of the element associated with the current event or
+ * {@code -1} if no element is associated with the current event.
+ */
+ public int getAttributeCount() {
+ if (mCurrentEvent != EVENT_START_ELEMENT) {
+ return -1;
+ }
+
+ return mCurrentElementAttributeCount;
+ }
+
+ /**
+ * Returns the resource ID corresponding to the name of the specified attribute of the current
+ * element or {@code 0} if the name is not associated with a resource ID.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public int getAttributeNameResourceId(int index) throws XmlParserException {
+ return getAttribute(index).getNameResourceId();
+ }
+
+ /**
+ * Returns the name of the specified attribute of the current element.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public String getAttributeName(int index) throws XmlParserException {
+ return getAttribute(index).getName();
+ }
+
+ /**
+ * Returns the name of the specified attribute of the current element or an empty string if
+ * the attribute is not associated with a namespace.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public String getAttributeNamespace(int index) throws XmlParserException {
+ return getAttribute(index).getNamespace();
+ }
+
+ /**
+ * Returns the value type of the specified attribute of the current element. See
+ * {@code VALUE_TYPE_...} constants.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public int getAttributeValueType(int index) throws XmlParserException {
+ int type = getAttribute(index).getValueType();
+ switch (type) {
+ case Attribute.TYPE_STRING:
+ return VALUE_TYPE_STRING;
+ case Attribute.TYPE_INT_DEC:
+ case Attribute.TYPE_INT_HEX:
+ return VALUE_TYPE_INT;
+ case Attribute.TYPE_REFERENCE:
+ return VALUE_TYPE_REFERENCE;
+ case Attribute.TYPE_INT_BOOLEAN:
+ return VALUE_TYPE_BOOLEAN;
+ default:
+ return VALUE_TYPE_UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Returns the integer value of the specified attribute of the current element. See
+ * {@code VALUE_TYPE_...} constants.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event.
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public int getAttributeIntValue(int index) throws XmlParserException {
+ return getAttribute(index).getIntValue();
+ }
+
+ /**
+ * Returns the boolean value of the specified attribute of the current element. See
+ * {@code VALUE_TYPE_...} constants.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event.
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public boolean getAttributeBooleanValue(int index) throws XmlParserException {
+ return getAttribute(index).getBooleanValue();
+ }
+
+ /**
+ * Returns the string value of the specified attribute of the current element. See
+ * {@code VALUE_TYPE_...} constants.
+ *
+ * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
+ * {@code start element} event.
+ * @throws XmlParserException if a parsing error is occurred
+ */
+ public String getAttributeStringValue(int index) throws XmlParserException {
+ return getAttribute(index).getStringValue();
+ }
+
+ private Attribute getAttribute(int index) {
+ if (mCurrentEvent != EVENT_START_ELEMENT) {
+ throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
+ }
+ if (index < 0) {
+ throw new IndexOutOfBoundsException("index must be >= 0");
+ }
+ if (index >= mCurrentElementAttributeCount) {
+ throw new IndexOutOfBoundsException(
+ "index must be <= attr count (" + mCurrentElementAttributeCount + ")");
+ }
+ parseCurrentElementAttributesIfNotParsed();
+ return mCurrentElementAttributes.get(index);
+ }
+
+ /**
+ * Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
+ */
+ public int next() throws XmlParserException {
+ // Decrement depth if the previous event was "end element".
+ if (mCurrentEvent == EVENT_END_ELEMENT) {
+ mDepth--;
+ }
+
+ // Read events from document, ignoring events that we don't report to caller. Stop at the
+ // earliest event which we report to caller.
+ while (mXml.hasRemaining()) {
+ Chunk chunk = Chunk.get(mXml);
+ if (chunk == null) {
+ break;
+ }
+ switch (chunk.getType()) {
+ case Chunk.TYPE_STRING_POOL:
+ if (mStringPool != null) {
+ throw new XmlParserException("Multiple string pools not supported");
+ }
+ mStringPool = new StringPool(chunk);
+ break;
+
+ case Chunk.RES_XML_TYPE_START_ELEMENT:
+ {
+ if (mStringPool == null) {
+ throw new XmlParserException(
+ "Named element encountered before string pool");
+ }
+ ByteBuffer contents = chunk.getContents();
+ if (contents.remaining() < 20) {
+ throw new XmlParserException(
+ "Start element chunk too short. Need at least 20 bytes. Available: "
+ + contents.remaining() + " bytes");
+ }
+ long nsId = getUnsignedInt32(contents);
+ long nameId = getUnsignedInt32(contents);
+ int attrStartOffset = getUnsignedInt16(contents);
+ int attrSizeBytes = getUnsignedInt16(contents);
+ int attrCount = getUnsignedInt16(contents);
+ long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
+ contents.position(0);
+ if (attrStartOffset > contents.remaining()) {
+ throw new XmlParserException(
+ "Attributes start offset out of bounds: " + attrStartOffset
+ + ", max: " + contents.remaining());
+ }
+ if (attrEndOffset > contents.remaining()) {
+ throw new XmlParserException(
+ "Attributes end offset out of bounds: " + attrEndOffset
+ + ", max: " + contents.remaining());
+ }
+
+ mCurrentElementName = mStringPool.getString(nameId);
+ mCurrentElementNamespace =
+ (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
+ mCurrentElementAttributeCount = attrCount;
+ mCurrentElementAttributes = null;
+ mCurrentElementAttrSizeBytes = attrSizeBytes;
+ mCurrentElementAttributesContents =
+ sliceFromTo(contents, attrStartOffset, attrEndOffset);
+
+ mDepth++;
+ mCurrentEvent = EVENT_START_ELEMENT;
+ return mCurrentEvent;
+ }
+
+ case Chunk.RES_XML_TYPE_END_ELEMENT:
+ {
+ if (mStringPool == null) {
+ throw new XmlParserException(
+ "Named element encountered before string pool");
+ }
+ ByteBuffer contents = chunk.getContents();
+ if (contents.remaining() < 8) {
+ throw new XmlParserException(
+ "End element chunk too short. Need at least 8 bytes. Available: "
+ + contents.remaining() + " bytes");
+ }
+ long nsId = getUnsignedInt32(contents);
+ long nameId = getUnsignedInt32(contents);
+ mCurrentElementName = mStringPool.getString(nameId);
+ mCurrentElementNamespace =
+ (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
+ mCurrentEvent = EVENT_END_ELEMENT;
+ mCurrentElementAttributes = null;
+ mCurrentElementAttributesContents = null;
+ return mCurrentEvent;
+ }
+ case Chunk.RES_XML_TYPE_RESOURCE_MAP:
+ if (mResourceMap != null) {
+ throw new XmlParserException("Multiple resource maps not supported");
+ }
+ mResourceMap = new ResourceMap(chunk);
+ break;
+ default:
+ // Unknown chunk type -- ignore
+ break;
+ }
+ }
+
+ mCurrentEvent = EVENT_END_DOCUMENT;
+ return mCurrentEvent;
+ }
+
+ private void parseCurrentElementAttributesIfNotParsed() {
+ if (mCurrentElementAttributes != null) {
+ return;
+ }
+ mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
+ for (int i = 0; i < mCurrentElementAttributeCount; i++) {
+ int startPosition = i * mCurrentElementAttrSizeBytes;
+ ByteBuffer attr =
+ sliceFromTo(
+ mCurrentElementAttributesContents,
+ startPosition,
+ startPosition + mCurrentElementAttrSizeBytes);
+ long nsId = getUnsignedInt32(attr);
+ long nameId = getUnsignedInt32(attr);
+ attr.position(attr.position() + 7); // skip ignored fields
+ int valueType = getUnsignedInt8(attr);
+ long valueData = getUnsignedInt32(attr);
+ mCurrentElementAttributes.add(
+ new Attribute(
+ nsId,
+ nameId,
+ valueType,
+ (int) valueData,
+ mStringPool,
+ mResourceMap));
+ }
+ }
+
+ private static class Attribute {
+ private static final int TYPE_REFERENCE = 1;
+ private static final int TYPE_STRING = 3;
+ private static final int TYPE_INT_DEC = 0x10;
+ private static final int TYPE_INT_HEX = 0x11;
+ private static final int TYPE_INT_BOOLEAN = 0x12;
+
+ private final long mNsId;
+ private final long mNameId;
+ private final int mValueType;
+ private final int mValueData;
+ private final StringPool mStringPool;
+ private final ResourceMap mResourceMap;
+
+ private Attribute(
+ long nsId,
+ long nameId,
+ int valueType,
+ int valueData,
+ StringPool stringPool,
+ ResourceMap resourceMap) {
+ mNsId = nsId;
+ mNameId = nameId;
+ mValueType = valueType;
+ mValueData = valueData;
+ mStringPool = stringPool;
+ mResourceMap = resourceMap;
+ }
+
+ public int getNameResourceId() {
+ return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
+ }
+
+ public String getName() throws XmlParserException {
+ return mStringPool.getString(mNameId);
+ }
+
+ public String getNamespace() throws XmlParserException {
+ return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
+ }
+
+ public int getValueType() {
+ return mValueType;
+ }
+
+ public int getIntValue() throws XmlParserException {
+ switch (mValueType) {
+ case TYPE_REFERENCE:
+ case TYPE_INT_DEC:
+ case TYPE_INT_HEX:
+ case TYPE_INT_BOOLEAN:
+ return mValueData;
+ default:
+ throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
+ }
+ }
+
+ public boolean getBooleanValue() throws XmlParserException {
+ switch (mValueType) {
+ case TYPE_INT_BOOLEAN:
+ return mValueData != 0;
+ default:
+ throw new XmlParserException(
+ "Cannot coerce to boolean: value type " + mValueType);
+ }
+ }
+
+ public String getStringValue() throws XmlParserException {
+ switch (mValueType) {
+ case TYPE_STRING:
+ return mStringPool.getString(mValueData & 0xffffffffL);
+ case TYPE_INT_DEC:
+ return Integer.toString(mValueData);
+ case TYPE_INT_HEX:
+ return "0x" + Integer.toHexString(mValueData);
+ case TYPE_INT_BOOLEAN:
+ return Boolean.toString(mValueData != 0);
+ case TYPE_REFERENCE:
+ return "@" + Integer.toHexString(mValueData);
+ default:
+ throw new XmlParserException(
+ "Cannot coerce to string: value type " + mValueType);
+ }
+ }
+ }
+
+ /**
+ * Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
+ * contents.
+ */
+ private static class Chunk {
+ public static final int TYPE_STRING_POOL = 1;
+ public static final int TYPE_RES_XML = 3;
+ public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
+ public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
+ public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
+
+ static final int HEADER_MIN_SIZE_BYTES = 8;
+
+ private final int mType;
+ private final ByteBuffer mHeader;
+ private final ByteBuffer mContents;
+
+ public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
+ mType = type;
+ mHeader = header;
+ mContents = contents;
+ }
+
+ public ByteBuffer getContents() {
+ ByteBuffer result = mContents.slice();
+ result.order(mContents.order());
+ return result;
+ }
+
+ public ByteBuffer getHeader() {
+ ByteBuffer result = mHeader.slice();
+ result.order(mHeader.order());
+ return result;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Consumes the chunk located at the current position of the input and returns the chunk
+ * or {@code null} if there is no chunk left in the input.
+ *
+ * @throws XmlParserException if the chunk is malformed
+ */
+ public static Chunk get(ByteBuffer input) throws XmlParserException {
+ if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
+ // Android ignores the last chunk if its header is too big to fit into the file
+ input.position(input.limit());
+ return null;
+ }
+
+ int originalPosition = input.position();
+ int type = getUnsignedInt16(input);
+ int headerSize = getUnsignedInt16(input);
+ long chunkSize = getUnsignedInt32(input);
+ long chunkRemaining = chunkSize - 8;
+ if (chunkRemaining > input.remaining()) {
+ // Android ignores the last chunk if it's too big to fit into the file
+ input.position(input.limit());
+ return null;
+ }
+ if (headerSize < HEADER_MIN_SIZE_BYTES) {
+ throw new XmlParserException(
+ "Malformed chunk: header too short: " + headerSize + " bytes");
+ } else if (headerSize > chunkSize) {
+ throw new XmlParserException(
+ "Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
+ + chunkSize + " bytes");
+ }
+ int contentStartPosition = originalPosition + headerSize;
+ long chunkEndPosition = originalPosition + chunkSize;
+ Chunk chunk =
+ new Chunk(
+ type,
+ sliceFromTo(input, originalPosition, contentStartPosition),
+ sliceFromTo(input, contentStartPosition, chunkEndPosition));
+ input.position((int) chunkEndPosition);
+ return chunk;
+ }
+ }
+
+ /**
+ * String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
+ */
+ private static class StringPool {
+ private static final int FLAG_UTF8 = 1 << 8;
+
+ private final ByteBuffer mChunkContents;
+ private final ByteBuffer mStringsSection;
+ private final int mStringCount;
+ private final boolean mUtf8Encoded;
+ private final Map<Integer, String> mCachedStrings = new HashMap<>();
+
+ /**
+ * Constructs a new string pool from the provided chunk.
+ *
+ * @throws XmlParserException if a parsing error occurred
+ */
+ public StringPool(Chunk chunk) throws XmlParserException {
+ ByteBuffer header = chunk.getHeader();
+ int headerSizeBytes = header.remaining();
+ header.position(Chunk.HEADER_MIN_SIZE_BYTES);
+ if (header.remaining() < 20) {
+ throw new XmlParserException(
+ "XML chunk's header too short. Required at least 20 bytes. Available: "
+ + header.remaining() + " bytes");
+ }
+ long stringCount = getUnsignedInt32(header);
+ if (stringCount > Integer.MAX_VALUE) {
+ throw new XmlParserException("Too many strings: " + stringCount);
+ }
+ mStringCount = (int) stringCount;
+ long styleCount = getUnsignedInt32(header);
+ if (styleCount > Integer.MAX_VALUE) {
+ throw new XmlParserException("Too many styles: " + styleCount);
+ }
+ long flags = getUnsignedInt32(header);
+ long stringsStartOffset = getUnsignedInt32(header);
+ long stylesStartOffset = getUnsignedInt32(header);
+
+ ByteBuffer contents = chunk.getContents();
+ if (mStringCount > 0) {
+ int stringsSectionStartOffsetInContents =
+ (int) (stringsStartOffset - headerSizeBytes);
+ int stringsSectionEndOffsetInContents;
+ if (styleCount > 0) {
+ // Styles section follows the strings section
+ if (stylesStartOffset < stringsStartOffset) {
+ throw new XmlParserException(
+ "Styles offset (" + stylesStartOffset + ") < strings offset ("
+ + stringsStartOffset + ")");
+ }
+ stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
+ } else {
+ stringsSectionEndOffsetInContents = contents.remaining();
+ }
+ mStringsSection =
+ sliceFromTo(
+ contents,
+ stringsSectionStartOffsetInContents,
+ stringsSectionEndOffsetInContents);
+ } else {
+ mStringsSection = ByteBuffer.allocate(0);
+ }
+
+ mUtf8Encoded = (flags & FLAG_UTF8) != 0;
+ mChunkContents = contents;
+ }
+
+ /**
+ * Returns the string located at the specified {@code 0}-based index in this pool.
+ *
+ * @throws XmlParserException if the string does not exist or cannot be decoded
+ */
+ public String getString(long index) throws XmlParserException {
+ if (index < 0) {
+ throw new XmlParserException("Unsuported string index: " + index);
+ } else if (index >= mStringCount) {
+ throw new XmlParserException(
+ "Unsuported string index: " + index + ", max: " + (mStringCount - 1));
+ }
+
+ int idx = (int) index;
+ String result = mCachedStrings.get(idx);
+ if (result != null) {
+ return result;
+ }
+
+ long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
+ if (offsetInStringsSection >= mStringsSection.capacity()) {
+ throw new XmlParserException(
+ "Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
+ + ", max: " + (mStringsSection.capacity() - 1));
+ }
+ mStringsSection.position((int) offsetInStringsSection);
+ result =
+ (mUtf8Encoded)
+ ? getLengthPrefixedUtf8EncodedString(mStringsSection)
+ : getLengthPrefixedUtf16EncodedString(mStringsSection);
+ mCachedStrings.put(idx, result);
+ return result;
+ }
+
+ private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
+ throws XmlParserException {
+ // If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
+ // Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
+ // of supported values is 0 to 0x7fffffff inclusive.
+ int lengthChars = getUnsignedInt16(encoded);
+ if ((lengthChars & 0x8000) != 0) {
+ lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
+ }
+ if (lengthChars > Integer.MAX_VALUE / 2) {
+ throw new XmlParserException("String too long: " + lengthChars + " uint16s");
+ }
+ int lengthBytes = lengthChars * 2;
+
+ byte[] arr;
+ int arrOffset;
+ if (encoded.hasArray()) {
+ arr = encoded.array();
+ arrOffset = encoded.arrayOffset() + encoded.position();
+ encoded.position(encoded.position() + lengthBytes);
+ } else {
+ arr = new byte[lengthBytes];
+ arrOffset = 0;
+ encoded.get(arr);
+ }
+ // Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
+ // array of bytes is NULL terminated.
+ if ((arr[arrOffset + lengthBytes] != 0)
+ || (arr[arrOffset + lengthBytes + 1] != 0)) {
+ throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
+ }
+ try {
+ return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-16LE character encoding not supported", e);
+ }
+ }
+
+ private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
+ throws XmlParserException {
+ // If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
+ // it is stored as a big-endian uint16 with highest bit set. Thus, the range of
+ // supported values is 0 to 0x7fff inclusive.
+
+ // Skip UTF-16 encoded length (in uint16s)
+ int lengthBytes = getUnsignedInt8(encoded);
+ if ((lengthBytes & 0x80) != 0) {
+ lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
+ }
+
+ // Read UTF-8 encoded length (in bytes)
+ lengthBytes = getUnsignedInt8(encoded);
+ if ((lengthBytes & 0x80) != 0) {
+ lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
+ }
+
+ byte[] arr;
+ int arrOffset;
+ if (encoded.hasArray()) {
+ arr = encoded.array();
+ arrOffset = encoded.arrayOffset() + encoded.position();
+ encoded.position(encoded.position() + lengthBytes);
+ } else {
+ arr = new byte[lengthBytes];
+ arrOffset = 0;
+ encoded.get(arr);
+ }
+ // Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
+ // of bytes is NULL terminated.
+ if (arr[arrOffset + lengthBytes] != 0) {
+ throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
+ }
+ try {
+ return new String(arr, arrOffset, lengthBytes, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 character encoding not supported", e);
+ }
+ }
+ }
+
+ /**
+ * Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
+ * map.
+ */
+ private static class ResourceMap {
+ private final ByteBuffer mChunkContents;
+ private final int mEntryCount;
+
+ /**
+ * Constructs a new resource map from the provided chunk.
+ *
+ * @throws XmlParserException if a parsing error occurred
+ */
+ public ResourceMap(Chunk chunk) throws XmlParserException {
+ mChunkContents = chunk.getContents().slice();
+ mChunkContents.order(chunk.getContents().order());
+ // Each entry of the map is four bytes long, containing the int32 resource ID.
+ mEntryCount = mChunkContents.remaining() / 4;
+ }
+
+ /**
+ * Returns the resource ID located at the specified {@code 0}-based index in this pool or
+ * {@code 0} if the index is out of range.
+ */
+ public int getResourceId(long index) {
+ if ((index < 0) || (index >= mEntryCount)) {
+ return 0;
+ }
+ int idx = (int) index;
+ // Each entry of the map is four bytes long, containing the int32 resource ID.
+ return mChunkContents.getInt(idx * 4);
+ }
+ }
+
+ /**
+ * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+ * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+ * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+ * buffer's byte order.
+ */
+ private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
+ if (start < 0) {
+ throw new IllegalArgumentException("start: " + start);
+ }
+ if (end < start) {
+ throw new IllegalArgumentException("end < start: " + end + " < " + start);
+ }
+ int capacity = source.capacity();
+ if (end > source.capacity()) {
+ throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+ }
+ return sliceFromTo(source, (int) start, (int) end);
+ }
+
+ /**
+ * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+ * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+ * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+ * buffer's byte order.
+ */
+ private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+ if (start < 0) {
+ throw new IllegalArgumentException("start: " + start);
+ }
+ if (end < start) {
+ throw new IllegalArgumentException("end < start: " + end + " < " + start);
+ }
+ int capacity = source.capacity();
+ if (end > source.capacity()) {
+ throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+ }
+ int originalLimit = source.limit();
+ int originalPosition = source.position();
+ try {
+ source.position(0);
+ source.limit(end);
+ source.position(start);
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ return result;
+ } finally {
+ source.position(0);
+ source.limit(originalLimit);
+ source.position(originalPosition);
+ }
+ }
+
+ private static int getUnsignedInt8(ByteBuffer buffer) {
+ return buffer.get() & 0xff;
+ }
+
+ private static int getUnsignedInt16(ByteBuffer buffer) {
+ return buffer.getShort() & 0xffff;
+ }
+
+ private static long getUnsignedInt32(ByteBuffer buffer) {
+ return buffer.getInt() & 0xffffffffL;
+ }
+
+ private static long getUnsignedInt32(ByteBuffer buffer, int position) {
+ return buffer.getInt(position) & 0xffffffffL;
+ }
+
+ /**
+ * Indicates that an error occurred while parsing a document.
+ */
+ public static class XmlParserException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public XmlParserException(String message) {
+ super(message);
+ }
+
+ public XmlParserException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java
new file mode 100644
index 0000000000..6151351b2b
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+import com.android.apksig.ApkVerificationIssue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base implementation of an APK signature verification result.
+ */
+public class ApkSigResult {
+ public final int signatureSchemeVersion;
+
+ /** Whether the APK's Signature Scheme signature verifies. */
+ public boolean verified;
+
+ public final List<ApkSignerInfo> mSigners = new ArrayList<>();
+ private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+ private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+
+ public ApkSigResult(int signatureSchemeVersion) {
+ this.signatureSchemeVersion = signatureSchemeVersion;
+ }
+
+ /**
+ * Returns {@code true} if this result encountered errors during verification.
+ */
+ public boolean containsErrors() {
+ if (!mErrors.isEmpty()) {
+ return true;
+ }
+ if (!mSigners.isEmpty()) {
+ for (ApkSignerInfo signer : mSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if this result encountered warnings during verification.
+ */
+ public boolean containsWarnings() {
+ if (!mWarnings.isEmpty()) {
+ return true;
+ }
+ if (!mSigners.isEmpty()) {
+ for (ApkSignerInfo signer : mSigners) {
+ if (signer.containsWarnings()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code
+ * issueId} and {@code params}.
+ */
+ public void addError(int issueId, Object... parameters) {
+ mErrors.add(new ApkVerificationIssue(issueId, parameters));
+ }
+
+ /**
+ * Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code
+ * issueId} and {@code params}.
+ */
+ public void addWarning(int issueId, Object... parameters) {
+ mWarnings.add(new ApkVerificationIssue(issueId, parameters));
+ }
+
+ /**
+ * Returns the errors encountered during verification.
+ */
+ public List<? extends ApkVerificationIssue> getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns the warnings encountered during verification.
+ */
+ public List<? extends ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
new file mode 100644
index 0000000000..3e7934195f
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+import com.android.apksig.ApkVerificationIssue;
+
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base implementation of an APK signer.
+ */
+public class ApkSignerInfo {
+ public int index;
+ public long timestamp;
+ public List<X509Certificate> certs = new ArrayList<>();
+ public List<X509Certificate> certificateLineage = new ArrayList<>();
+
+ private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
+ private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
+ private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+
+ /**
+ * Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code
+ * issueId} and {@code params}.
+ */
+ public void addError(int issueId, Object... params) {
+ mErrors.add(new ApkVerificationIssue(issueId, params));
+ }
+
+ /**
+ * Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code
+ * issueId} and {@code params}.
+ */
+ public void addWarning(int issueId, Object... params) {
+ mWarnings.add(new ApkVerificationIssue(issueId, params));
+ }
+
+ /**
+ * Adds a new {@link ApkVerificationIssue} as an info message to this signer config using the
+ * provided {@code issueId} and {@code params}.
+ */
+ public void addInfoMessage(int issueId, Object... params) {
+ mInfoMessages.add(new ApkVerificationIssue(issueId, params));
+ }
+
+ /**
+ * Returns {@code true} if any errors were encountered during verification for this signer.
+ */
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ /**
+ * Returns {@code true} if any warnings were encountered during verification for this signer.
+ */
+ public boolean containsWarnings() {
+ return !mWarnings.isEmpty();
+ }
+
+ /**
+ * Returns {@code true} if any info messages were encountered during verification of this
+ * signer.
+ */
+ public boolean containsInfoMessages() {
+ return !mInfoMessages.isEmpty();
+ }
+
+ /**
+ * Returns the errors encountered during verification for this signer.
+ */
+ public List<? extends ApkVerificationIssue> getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns the warnings encountered during verification for this signer.
+ */
+ public List<? extends ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+
+ /**
+ * Returns the info messages encountered during verification of this signer.
+ */
+ public List<? extends ApkVerificationIssue> getInfoMessages() {
+ return mInfoMessages;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
new file mode 100644
index 0000000000..127ac24d3f
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -0,0 +1,1444 @@
+/*
+ * 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 static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA256;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA512;
+import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.SigningCertificateLineage;
+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.Asn1DecodingException;
+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.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.ByteBufferDataSource;
+import com.android.apksig.internal.util.ChainedDataSource;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.VerityTreeBuilder;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.internal.x509.RSAPublicKey;
+import com.android.apksig.internal.x509.SubjectPublicKeyInfo;
+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.math.BigInteger;
+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.CertificateException;
+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.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+import javax.security.auth.x500.X500Principal;
+
+public class ApkSigningBlockUtils {
+
+ private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
+ public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
+ private 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,
+ };
+ public static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
+
+ private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS =
+ {CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256};
+
+ public static final int VERSION_SOURCE_STAMP = 0;
+ public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
+
+ /**
+ * 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) {
+ return ApkSigningBlockUtilsLite.compareSignatureAlgorithm(alg1, alg2);
+ }
+
+ /**
+ * 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}.
+ *
+ * <p>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}.
+ *
+ * <p>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<ContentDigestAlgorithm> 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<ContentDigestAlgorithm, byte[]> actualContentDigests;
+ try {
+ actualContentDigests =
+ computeContentDigests(
+ executor,
+ contentDigestAlgorithms,
+ beforeApkSigningBlock,
+ centralDir,
+ new ByteBufferDataSource(modifiedEocd));
+ // Special checks for the verity algorithm requirements.
+ if (actualContentDigests.containsKey(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 {
+ try {
+ return ApkSigningBlockUtilsLite.findApkSignatureSchemeBlock(apkSigningBlock, blockId);
+ } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
+ throw new SignatureNotFoundException(e.getMessage());
+ }
+ }
+
+ public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+ ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(buffer);
+ }
+
+ public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
+ return ApkSigningBlockUtilsLite.getLengthPrefixedSlice(source);
+ }
+
+ public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
+ return ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(buf);
+ }
+
+ public static String toHex(byte[] value) {
+ return ApkSigningBlockUtilsLite.toHex(value);
+ }
+
+ public static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
+ RunnablesExecutor executor,
+ Set<ContentDigestAlgorithm> digestAlgorithms,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd) throws IOException, NoSuchAlgorithmException, DigestException {
+ Map<ContentDigestAlgorithm, byte[]> contentDigests = new HashMap<>();
+ Set<ContentDigestAlgorithm> oneMbChunkBasedAlgorithm = new HashSet<>();
+ for (ContentDigestAlgorithm digestAlgorithm : digestAlgorithms) {
+ if (digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
+ || digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) {
+ oneMbChunkBasedAlgorithm.add(digestAlgorithm);
+ }
+ }
+ computeOneMbChunkContentDigests(
+ executor,
+ oneMbChunkBasedAlgorithm,
+ new DataSource[] { beforeCentralDir, centralDir, eocd },
+ contentDigests);
+
+ if (digestAlgorithms.contains(VERITY_CHUNKED_SHA256)) {
+ computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests);
+ }
+ return contentDigests;
+ }
+
+ static void computeOneMbChunkContentDigests(
+ Set<ContentDigestAlgorithm> digestAlgorithms,
+ DataSource[] contents,
+ Map<ContentDigestAlgorithm, byte[]> 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<ContentDigestAlgorithm> digestAlgorithms,
+ DataSource[] contents,
+ Map<ContentDigestAlgorithm, byte[]> 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<ChunkDigests> 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> chunkDigests;
+ private final List<MessageDigest> messageDigests;
+ private final DataSink mdSink;
+
+ private ChunkDigester(ChunkSupplier dataSupplier, List<ChunkDigests> 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()) {
+ int size = chunk.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(size, chunkContentPrefix, 1);
+ mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length);
+
+ // Then update with the chunk data.
+ mdSink.consume(chunk.data);
+
+ // 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<ChunkSupplier.Chunk> {
+ 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 = (int) (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;
+ long 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);
+
+ final int size = (int)remainingSize;
+ final ByteBuffer buffer = ByteBuffer.allocate(size);
+ try {
+ dataSources[dataSourceIndex].copyTo(
+ dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, size,
+ buffer);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to read chunk", e);
+ }
+ buffer.rewind();
+
+ return new Chunk(index, buffer, size);
+ }
+
+ static class Chunk {
+ private final int chunkIndex;
+ private final ByteBuffer data;
+ private final int size;
+
+ private Chunk(int chunkIndex, ByteBuffer data, int size) {
+ this.chunkIndex = chunkIndex;
+ this.data = data;
+ this.size = size;
+ }
+ }
+ }
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir,
+ DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
+ throws IOException, NoSuchAlgorithmException {
+ ByteBuffer encoded = createVerityDigestBuffer(true);
+ // Use 0s as salt for now. This also needs to be consistent in the fsverify header for
+ // kernel to use.
+ try (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(VERITY_CHUNKED_SHA256, encoded.array());
+ }
+ }
+
+ private static ByteBuffer createVerityDigestBuffer(boolean includeSourceDataSize) {
+ // FORMAT:
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint8[32] Merkle tree root hash of SHA-256
+ // * @+32 bytes int64 (optional) Length of source data
+ int backBufferSize =
+ VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes();
+ if (includeSourceDataSize) {
+ backBufferSize += Long.SIZE / Byte.SIZE;
+ }
+ ByteBuffer encoded = ByteBuffer.allocate(backBufferSize);
+ encoded.order(ByteOrder.LITTLE_ENDIAN);
+ return encoded;
+ }
+
+ public static class VerityTreeAndDigest {
+ public final ContentDigestAlgorithm contentDigestAlgorithm;
+ public final byte[] rootHash;
+ public final byte[] tree;
+
+ VerityTreeAndDigest(ContentDigestAlgorithm contentDigestAlgorithm, byte[] rootHash,
+ byte[] tree) {
+ this.contentDigestAlgorithm = contentDigestAlgorithm;
+ this.rootHash = rootHash;
+ this.tree = tree;
+ }
+ }
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ public static VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource)
+ throws IOException, NoSuchAlgorithmException {
+ ByteBuffer encoded = createVerityDigestBuffer(false);
+ // Use 0s as salt for now. This also needs to be consistent in the fsverify header for
+ // kernel to use.
+ try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) {
+ ByteBuffer tree = builder.generateVerityTree(dataSource);
+ byte[] rootHash = builder.getRootHashFromTree(tree);
+ encoded.put(rootHash);
+ return new VerityTreeAndDigest(VERITY_CHUNKED_SHA256, encoded.array(), tree.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 the key is an RSA key check for a negative modulus
+ String keyAlgorithm = publicKey.getAlgorithm();
+ if ("RSA".equals(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
+ try {
+ // Parse the encoded public key into the separate elements of the
+ // SubjectPublicKeyInfo to obtain the SubjectPublicKey.
+ ByteBuffer encodedPublicKeyBuffer = ByteBuffer.wrap(encodedPublicKey);
+ SubjectPublicKeyInfo subjectPublicKeyInfo = Asn1BerParser.parse(
+ encodedPublicKeyBuffer, SubjectPublicKeyInfo.class);
+ // The SubjectPublicKey is encoded as a bit string within the
+ // SubjectPublicKeyInfo. The first byte of the encoding is the number of padding
+ // bits; store this and decode the rest of the bit string into the RSA modulus
+ // and exponent.
+ ByteBuffer subjectPublicKeyBuffer = subjectPublicKeyInfo.subjectPublicKey;
+ byte padding = subjectPublicKeyBuffer.get();
+ RSAPublicKey rsaPublicKey = Asn1BerParser.parse(subjectPublicKeyBuffer,
+ RSAPublicKey.class);
+ // if the modulus is negative then attempt to reencode it with a leading 0 sign
+ // byte.
+ if (rsaPublicKey.modulus.compareTo(BigInteger.ZERO) < 0) {
+ // A negative modulus indicates the leading bit in the integer is 1. Per
+ // ASN.1 encoding rules to encode a positive integer with the leading bit
+ // set to 1 a byte containing all zeros should precede the integer encoding.
+ byte[] encodedModulus = rsaPublicKey.modulus.toByteArray();
+ byte[] reencodedModulus = new byte[encodedModulus.length + 1];
+ reencodedModulus[0] = 0;
+ System.arraycopy(encodedModulus, 0, reencodedModulus, 1,
+ encodedModulus.length);
+ rsaPublicKey.modulus = new BigInteger(reencodedModulus);
+ // Once the modulus has been corrected reencode the RSAPublicKey, then
+ // restore the padding value in the bit string and reencode the entire
+ // SubjectPublicKeyInfo to be returned to the caller.
+ byte[] reencodedRSAPublicKey = Asn1DerEncoder.encode(rsaPublicKey);
+ byte[] reencodedSubjectPublicKey =
+ new byte[reencodedRSAPublicKey.length + 1];
+ reencodedSubjectPublicKey[0] = padding;
+ System.arraycopy(reencodedRSAPublicKey, 0, reencodedSubjectPublicKey, 1,
+ reencodedRSAPublicKey.length);
+ subjectPublicKeyInfo.subjectPublicKey = ByteBuffer.wrap(
+ reencodedSubjectPublicKey);
+ encodedPublicKey = Asn1DerEncoder.encode(subjectPublicKeyInfo);
+ }
+ } catch (Asn1DecodingException | Asn1EncodingException e) {
+ System.out.println("Caught a exception encoding the public key: " + e);
+ e.printStackTrace();
+ encodedPublicKey = null;
+ }
+ }
+ }
+ 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<byte[]> encodeCertificates(List<X509Certificate> certificates)
+ throws CertificateEncodingException {
+ List<byte[]> 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<byte[]> 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<Pair<Integer, byte[]>> sequence) {
+ return ApkSigningBlockUtilsLite
+ .encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(sequence);
+ }
+
+ /**
+ * 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 {
+ try {
+ return ApkSigningBlockUtilsLite.findSignature(apk, zipSections, blockId);
+ } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) {
+ throw new SignatureNotFoundException(e.getMessage());
+ }
+ }
+
+ /**
+ * 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<DataSource, Integer> 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<Pair<byte[], Integer>> apkSignatureSchemeBlockPairs) {
+ // FORMAT:
+ // uint64: size (excluding this field)
+ // repeated ID-value pairs:
+ // uint64: size (excluding this field)
+ // uint32: ID
+ // (size - 4) bytes: value
+ // (extra verity 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<byte[], Integer> 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<byte[], Integer> 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();
+ }
+
+ /**
+ * Returns the individual APK signature blocks within the provided {@code apkSigningBlock} in a
+ * {@code List} of {@code Pair} instances where the first element in the {@code Pair} is the
+ * contents / value of the signature block and the second element is the ID of the block.
+ *
+ * @throws IOException if an error is encountered reading the provided {@code apkSigningBlock}
+ */
+ public static List<Pair<byte[], Integer>> getApkSignatureBlocks(
+ DataSource apkSigningBlock) throws IOException {
+ // FORMAT:
+ // uint64: size (excluding this field)
+ // repeated ID-value pairs:
+ // uint64: size (excluding this field)
+ // uint32: ID
+ // (size - 4) bytes: value
+ // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
+ // uint64: size (same as the one above)
+ // uint128: magic
+ long apkSigningBlockSize = apkSigningBlock.size();
+ if (apkSigningBlock.size() > Integer.MAX_VALUE || apkSigningBlockSize < 32) {
+ throw new IllegalArgumentException(
+ "APK signing block size out of range: " + apkSigningBlockSize);
+ }
+ // Remove the header and footer from the signing block to iterate over only the repeated
+ // ID-value pairs.
+ ByteBuffer apkSigningBlockBuffer = apkSigningBlock.getByteBuffer(8,
+ (int) apkSigningBlock.size() - 32);
+ apkSigningBlockBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ List<Pair<byte[], Integer>> signatureBlocks = new ArrayList<>();
+ while (apkSigningBlockBuffer.hasRemaining()) {
+ long blockLength = apkSigningBlockBuffer.getLong();
+ if (blockLength > Integer.MAX_VALUE || blockLength < 4) {
+ throw new IllegalArgumentException(
+ "Block index " + (signatureBlocks.size() + 1) + " size out of range: "
+ + blockLength);
+ }
+ int blockId = apkSigningBlockBuffer.getInt();
+ // Since the block ID has already been read from the signature block read the next
+ // blockLength - 4 bytes as the value.
+ byte[] blockValue = new byte[(int) blockLength - 4];
+ apkSigningBlockBuffer.get(blockValue);
+ signatureBlocks.add(Pair.of(blockValue, blockId));
+ }
+ return signatureBlocks;
+ }
+
+ /**
+ * Returns the individual APK signers within the provided {@code signatureBlock} in a {@code
+ * List} of {@code Pair} instances where the first element is a {@code List} of {@link
+ * X509Certificate}s and the second element is a byte array of the individual signer's block.
+ *
+ * <p>This method supports any signature block that adheres to the following format up to the
+ * signing certificate(s):
+ * <pre>
+ * * length-prefixed sequence of length-prefixed signers
+ * * length-prefixed signed data
+ * * length-prefixed sequence of length-prefixed digests:
+ * * uint32: signature algorithm ID
+ * * length-prefixed bytes: digest of contents
+ * * length-prefixed sequence of certificates:
+ * * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+ * </pre>
+ *
+ * <p>Note, this is a convenience method to obtain any signers from an existing signature block;
+ * the signature of each signer will not be verified.
+ *
+ * @throws ApkFormatException if an error is encountered while parsing the provided {@code
+ * signatureBlock}
+ * @throws CertificateException if the signing certificate(s) within an individual signer block
+ * cannot be parsed
+ */
+ public static List<Pair<List<X509Certificate>, byte[]>> getApkSignatureBlockSigners(
+ byte[] signatureBlock) throws ApkFormatException, CertificateException {
+ ByteBuffer signatureBlockBuffer = ByteBuffer.wrap(signatureBlock);
+ signatureBlockBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ ByteBuffer signersBuffer = getLengthPrefixedSlice(signatureBlockBuffer);
+ List<Pair<List<X509Certificate>, byte[]>> signers = new ArrayList<>();
+ while (signersBuffer.hasRemaining()) {
+ // Parse the next signer block, save all of its bytes for the resulting List, and
+ // rewind the buffer to allow the signing certificate(s) to be parsed.
+ ByteBuffer signer = getLengthPrefixedSlice(signersBuffer);
+ byte[] signerBytes = new byte[signer.remaining()];
+ signer.get(signerBytes);
+ signer.rewind();
+
+ ByteBuffer signedData = getLengthPrefixedSlice(signer);
+ // The first length prefixed slice is the sequence of digests which are not required
+ // when obtaining the signing certificate(s).
+ getLengthPrefixedSlice(signedData);
+ ByteBuffer certificatesBuffer = getLengthPrefixedSlice(signedData);
+ List<X509Certificate> certificates = new ArrayList<>();
+ while (certificatesBuffer.hasRemaining()) {
+ int certLength = certificatesBuffer.getInt();
+ byte[] certBytes = new byte[certLength];
+ if (certLength > certificatesBuffer.remaining()) {
+ throw new IllegalArgumentException(
+ "Cert index " + (certificates.size() + 1) + " under signer index "
+ + (signers.size() + 1) + " size out of range: " + certLength);
+ }
+ certificatesBuffer.get(certBytes);
+ GuaranteedEncodedFormX509Certificate signerCert =
+ new GuaranteedEncodedFormX509Certificate(
+ X509CertificateUtils.generateCertificate(certBytes), certBytes);
+ certificates.add(signerCert);
+ }
+ signers.add(Pair.of(certificates, signerBytes));
+ }
+ return signers;
+ }
+
+ /**
+ * 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<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>>
+ computeContentDigests(
+ RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> 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<ContentDigestAlgorithm> contentDigestAlgorithms = new HashSet<>(1);
+ for (SignerConfig signerConfig : signerConfigs) {
+ for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+ contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm());
+ }
+ }
+
+ // Compute digests of APK contents.
+ Map<ContentDigestAlgorithm, byte[]> 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.
+ *
+ * <p>Each Android platform version typically verifies exactly one signature from the provided
+ * {@code signatures} set. This method returns the set of these signatures collected over all
+ * requested platform versions. As a result, the result may contain more than one signature.
+ *
+ * @throws NoSupportedSignaturesException if no supported signatures were
+ * found for an Android platform version in the range.
+ */
+ public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+ List<T> signatures, int minSdkVersion, int maxSdkVersion)
+ throws NoSupportedSignaturesException {
+ return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
+ }
+
+ /**
+ * Returns the subset of signatures which are expected to be verified by at least one Android
+ * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+ * guaranteed to contain at least one signature.
+ *
+ * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
+ * signature within the signing block using the standard JCA.
+ *
+ * <p>Each Android platform version typically verifies exactly one signature from the provided
+ * {@code signatures} set. This method returns the set of these signatures collected over all
+ * requested platform versions. As a result, the result may contain more than one signature.
+ *
+ * @throws NoSupportedSignaturesException if no supported signatures were
+ * found for an Android platform version in the range.
+ */
+ public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+ List<T> signatures, int minSdkVersion, int maxSdkVersion,
+ boolean onlyRequireJcaSupport) throws NoSupportedSignaturesException {
+ try {
+ return ApkSigningBlockUtilsLite.getSignaturesToVerify(signatures, minSdkVersion,
+ maxSdkVersion, onlyRequireJcaSupport);
+ } catch (NoApkSupportedSignaturesException e) {
+ throw new NoSupportedSignaturesException(e.getMessage());
+ }
+ }
+
+ public static class NoSupportedSignaturesException extends NoApkSupportedSignaturesException {
+ 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<Pair<Integer, byte[]>> generateSignaturesOverData(
+ SignerConfig signerConfig, byte[] data)
+ throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+ List<Pair<Integer, byte[]>> signatures =
+ new ArrayList<>(signerConfig.signatureAlgorithms.size());
+ PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+ for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+ Pair<String, ? extends AlgorithmParameterSpec> 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;
+ }
+
+ /**
+ * Wrap the signature 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
+ *
+ * @throws Asn1EncodingException if the ASN.1 structure could not be encoded
+ */
+ public static byte[] generatePkcs7DerEncodedMessage(
+ byte[] signatureBytes, ByteBuffer data, List<X509Certificate> signerCerts,
+ AlgorithmIdentifier digestAlgorithmId, AlgorithmIdentifier signatureAlgorithmId)
+ throws Asn1EncodingException, CertificateEncodingException {
+ SignerInfo signerInfo = new SignerInfo();
+ signerInfo.version = 1;
+ X509Certificate signingCert = signerCerts.get(0);
+ X500Principal signerCertIssuer = signingCert.getIssuerX500Principal();
+ signerInfo.sid =
+ new SignerIdentifier(
+ new IssuerAndSerialNumber(
+ new Asn1OpaqueObject(signerCertIssuer.getEncoded()),
+ signingCert.getSerialNumber()));
+
+ 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);
+ // If data is not null, data will be embedded as is in the result -- an attached pcsk7
+ signedData.encapContentInfo.content = 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);
+ }
+
+ /**
+ * Picks the correct v2/v3 digest for v4 signature verification.
+ *
+ * Keep in sync with pickBestDigestForV4 in framework's ApkSigningBlockUtils.
+ */
+ public static byte[] pickBestDigestForV4(Map<ContentDigestAlgorithm, byte[]> contentDigests) {
+ for (ContentDigestAlgorithm algo : V4_CONTENT_DIGEST_ALGORITHMS) {
+ if (contentDigests.containsKey(algo)) {
+ return contentDigests.get(algo);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 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<X509Certificate> certificates;
+
+ /**
+ * List of signature algorithms with which to sign.
+ */
+ public List<SignatureAlgorithm> signatureAlgorithms;
+
+ public int minSdkVersion;
+ public int maxSdkVersion;
+ public boolean signerTargetsDevRelease;
+ public SigningCertificateLineage signingCertificateLineage;
+ }
+
+ public static class Result extends ApkSigResult {
+ public SigningCertificateLineage signingCertificateLineage = null;
+ public final List<Result.SignerInfo> signers = new ArrayList<>();
+ private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>();
+ private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>();
+
+ public Result(int signatureSchemeVersion) {
+ super(signatureSchemeVersion);
+ }
+
+ public boolean containsErrors() {
+ if (!mErrors.isEmpty()) {
+ return true;
+ }
+ if (!signers.isEmpty()) {
+ for (Result.SignerInfo signer : signers) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public boolean containsWarnings() {
+ if (!mWarnings.isEmpty()) {
+ return true;
+ }
+ if (!signers.isEmpty()) {
+ for (Result.SignerInfo signer : signers) {
+ if (signer.containsWarnings()) {
+ 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));
+ }
+
+ @Override
+ public List<ApkVerifier.IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ @Override
+ public List<ApkVerifier.IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+
+ public static class SignerInfo extends ApkSignerInfo {
+ public List<ContentDigest> contentDigests = new ArrayList<>();
+ public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>();
+ public List<Signature> signatures = new ArrayList<>();
+ public Map<SignatureAlgorithm, byte[]> verifiedSignatures = new HashMap<>();
+ public List<AdditionalAttribute> additionalAttributes = new ArrayList<>();
+ public byte[] signedData;
+ public int minSdkVersion;
+ public int maxSdkVersion;
+ public SigningCertificateLineage signingCertificateLineage;
+
+ private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>();
+ private final List<ApkVerifier.IssueWithParams> 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 boolean containsWarnings() {
+ return !mWarnings.isEmpty();
+ }
+
+ public List<ApkVerifier.IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<ApkVerifier.IssueWithParams> 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 extends ApkSupportedSignature {
+ public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
+ super(algorithm, signature);
+ }
+ }
+
+ public static class SigningSchemeBlockAndDigests {
+ public final Pair<byte[], Integer> signingSchemeBlock;
+ public final Map<ContentDigestAlgorithm, byte[]> digestInfo;
+
+ public SigningSchemeBlockAndDigests(
+ Pair<byte[], Integer> signingSchemeBlock,
+ Map<ContentDigestAlgorithm, byte[]> digestInfo) {
+ this.signingSchemeBlock = signingSchemeBlock;
+ this.digestInfo = digestInfo;
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java
new file mode 100644
index 0000000000..40ae94798a
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkSigningBlockNotFoundException;
+import com.android.apksig.apk.ApkUtilsLite;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the
+ * utility functionality.
+ */
+public class ApkSigningBlockUtilsLite {
+ private ApkSigningBlockUtilsLite() {}
+
+ private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
+ /**
+ * Returns the APK Signature Scheme block contained in the provided APK file for the given ID
+ * and the additional information relevant for verifying the block against the file.
+ *
+ * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
+ * identifying the appropriate block to find, e.g. the APK Signature Scheme v2
+ * block ID.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
+ * @throws IOException if an I/O error occurs while reading the APK
+ */
+ public static SignatureInfo findSignature(
+ DataSource apk, ZipSections zipSections, int blockId)
+ throws IOException, SignatureNotFoundException {
+ // Find the APK Signing Block.
+ DataSource apkSigningBlock;
+ long apkSigningBlockOffset;
+ try {
+ ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo =
+ ApkUtilsLite.findApkSigningBlock(apk, zipSections);
+ apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
+ apkSigningBlock = apkSigningBlockInfo.getContents();
+ } catch (ApkSigningBlockNotFoundException e) {
+ throw new SignatureNotFoundException(e.getMessage(), e);
+ }
+ ByteBuffer apkSigningBlockBuf =
+ apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
+ apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
+
+ // Find the APK Signature Scheme Block inside the APK Signing Block.
+ ByteBuffer apkSignatureSchemeBlock =
+ findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId);
+ return new SignatureInfo(
+ apkSignatureSchemeBlock,
+ apkSigningBlockOffset,
+ zipSections.getZipCentralDirectoryOffset(),
+ zipSections.getZipEndOfCentralDirectoryOffset(),
+ zipSections.getZipEndOfCentralDirectory());
+ }
+
+ public static ByteBuffer findApkSignatureSchemeBlock(
+ ByteBuffer apkSigningBlock,
+ int blockId) throws SignatureNotFoundException {
+ checkByteOrderLittleEndian(apkSigningBlock);
+ // FORMAT:
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint64: size in bytes (excluding this field)
+ // * @+8 bytes pairs
+ // * @-24 bytes uint64: size in bytes (same as the one above)
+ // * @-16 bytes uint128: magic
+ ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
+
+ int entryCount = 0;
+ while (pairs.hasRemaining()) {
+ entryCount++;
+ if (pairs.remaining() < 8) {
+ throw new SignatureNotFoundException(
+ "Insufficient data to read size of APK Signing Block entry #" + entryCount);
+ }
+ long lenLong = pairs.getLong();
+ if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block entry #" + entryCount
+ + " size out of range: " + lenLong);
+ }
+ int len = (int) lenLong;
+ int nextEntryPos = pairs.position() + len;
+ if (len > pairs.remaining()) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block entry #" + entryCount + " size out of range: " + len
+ + ", available: " + pairs.remaining());
+ }
+ int id = pairs.getInt();
+ if (id == blockId) {
+ return getByteBuffer(pairs, len - 4);
+ }
+ pairs.position(nextEntryPos);
+ }
+
+ throw new SignatureNotFoundException(
+ "No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
+ }
+
+ public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+ if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+ throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+ }
+ }
+
+ /**
+ * Returns the subset of signatures which are expected to be verified by at least one Android
+ * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+ * guaranteed to contain at least one signature.
+ *
+ * <p>Each Android platform version typically verifies exactly one signature from the provided
+ * {@code signatures} set. This method returns the set of these signatures collected over all
+ * requested platform versions. As a result, the result may contain more than one signature.
+ *
+ * @throws NoApkSupportedSignaturesException if no supported signatures were
+ * found for an Android platform version in the range.
+ */
+ public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+ List<T> signatures, int minSdkVersion, int maxSdkVersion)
+ throws NoApkSupportedSignaturesException {
+ return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
+ }
+
+ /**
+ * Returns the subset of signatures which are expected to be verified by at least one Android
+ * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
+ * guaranteed to contain at least one signature.
+ *
+ * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
+ * signature within the signing block using the standard JCA.
+ *
+ * <p>Each Android platform version typically verifies exactly one signature from the provided
+ * {@code signatures} set. This method returns the set of these signatures collected over all
+ * requested platform versions. As a result, the result may contain more than one signature.
+ *
+ * @throws NoApkSupportedSignaturesException if no supported signatures were
+ * found for an Android platform version in the range.
+ */
+ public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
+ List<T> signatures, int minSdkVersion, int maxSdkVersion,
+ boolean onlyRequireJcaSupport) throws
+ NoApkSupportedSignaturesException {
+ // Pick the signature with the strongest algorithm at all required SDK versions, to mimic
+ // Android's behavior on those versions.
+ //
+ // Here we assume that, once introduced, a signature algorithm continues to be supported in
+ // all future Android versions. We also assume that the better-than relationship between
+ // algorithms is exactly the same on all Android platform versions (except that older
+ // platforms might support fewer algorithms). If these assumption are no longer true, the
+ // logic here will need to change accordingly.
+ Map<Integer, T>
+ bestSigAlgorithmOnSdkVersion = new HashMap<>();
+ int minProvidedSignaturesVersion = Integer.MAX_VALUE;
+ for (T sig : signatures) {
+ SignatureAlgorithm sigAlgorithm = sig.algorithm;
+ int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
+ : sigAlgorithm.getMinSdkVersion();
+ if (sigMinSdkVersion > maxSdkVersion) {
+ continue;
+ }
+ if (sigMinSdkVersion < minProvidedSignaturesVersion) {
+ minProvidedSignaturesVersion = sigMinSdkVersion;
+ }
+
+ T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
+ if ((candidate == null)
+ || (compareSignatureAlgorithm(
+ sigAlgorithm, candidate.algorithm) > 0)) {
+ bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
+ }
+ }
+
+ // Must have some supported signature algorithms for minSdkVersion.
+ if (minSdkVersion < minProvidedSignaturesVersion) {
+ throw new NoApkSupportedSignaturesException(
+ "Minimum provided signature version " + minProvidedSignaturesVersion +
+ " > minSdkVersion " + minSdkVersion);
+ }
+ if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
+ throw new NoApkSupportedSignaturesException("No supported signature");
+ }
+ List<T> signaturesToVerify =
+ new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
+ Collections.sort(
+ signaturesToVerify,
+ (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
+ return signaturesToVerify;
+ }
+
+ /**
+ * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
+ * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
+ */
+ public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
+ ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
+ ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
+ return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
+ }
+
+ /**
+ * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
+ * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
+ */
+ private static int compareContentDigestAlgorithm(
+ ContentDigestAlgorithm alg1,
+ ContentDigestAlgorithm alg2) {
+ switch (alg1) {
+ case CHUNKED_SHA256:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ return 0;
+ case CHUNKED_SHA512:
+ case VERITY_CHUNKED_SHA256:
+ return -1;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ case CHUNKED_SHA512:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ case VERITY_CHUNKED_SHA256:
+ return 1;
+ case CHUNKED_SHA512:
+ return 0;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ case VERITY_CHUNKED_SHA256:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ return 1;
+ case VERITY_CHUNKED_SHA256:
+ return 0;
+ case CHUNKED_SHA512:
+ return -1;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown alg1: " + alg1);
+ }
+ }
+
+ /**
+ * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+ * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+ * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+ * buffer's byte order.
+ */
+ private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+ if (start < 0) {
+ throw new IllegalArgumentException("start: " + start);
+ }
+ if (end < start) {
+ throw new IllegalArgumentException("end < start: " + end + " < " + start);
+ }
+ int capacity = source.capacity();
+ if (end > source.capacity()) {
+ throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+ }
+ int originalLimit = source.limit();
+ int originalPosition = source.position();
+ try {
+ source.position(0);
+ source.limit(end);
+ source.position(start);
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ return result;
+ } finally {
+ source.position(0);
+ source.limit(originalLimit);
+ source.position(originalPosition);
+ }
+ }
+
+ /**
+ * Relative <em>get</em> method for reading {@code size} number of bytes from the current
+ * position of this buffer.
+ *
+ * <p>This method reads the next {@code size} bytes at this buffer's current position,
+ * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
+ * {@code size}, byte order set to this buffer's byte order; and then increments the position by
+ * {@code size}.
+ */
+ private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
+ if (size < 0) {
+ throw new IllegalArgumentException("size: " + size);
+ }
+ int originalLimit = source.limit();
+ int position = source.position();
+ int limit = position + size;
+ if ((limit < position) || (limit > originalLimit)) {
+ throw new BufferUnderflowException();
+ }
+ source.limit(limit);
+ try {
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ source.position(limit);
+ return result;
+ } finally {
+ source.limit(originalLimit);
+ }
+ }
+
+ public static String toHex(byte[] value) {
+ StringBuilder sb = new StringBuilder(value.length * 2);
+ int len = value.length;
+ for (int i = 0; i < len; i++) {
+ int hi = (value[i] & 0xff) >>> 4;
+ int lo = value[i] & 0x0f;
+ sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
+ }
+ return sb.toString();
+ }
+
+ public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
+ if (source.remaining() < 4) {
+ throw new ApkFormatException(
+ "Remaining buffer too short to contain length of length-prefixed field"
+ + ". Remaining: " + source.remaining());
+ }
+ int len = source.getInt();
+ if (len < 0) {
+ throw new IllegalArgumentException("Negative length");
+ } else if (len > source.remaining()) {
+ throw new ApkFormatException(
+ "Length-prefixed field longer than remaining buffer"
+ + ". Field length: " + len + ", remaining: " + source.remaining());
+ }
+ return getByteBuffer(source, len);
+ }
+
+ public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
+ int len = buf.getInt();
+ if (len < 0) {
+ throw new ApkFormatException("Negative length");
+ } else if (len > buf.remaining()) {
+ throw new ApkFormatException(
+ "Underflow while reading length-prefixed value. Length: " + len
+ + ", available: " + buf.remaining());
+ }
+ byte[] result = new byte[len];
+ buf.get(result);
+ return result;
+ }
+
+ public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ List<Pair<Integer, byte[]>> sequence) {
+ int resultSize = 0;
+ for (Pair<Integer, byte[]> element : sequence) {
+ resultSize += 12 + element.getSecond().length;
+ }
+ ByteBuffer result = ByteBuffer.allocate(resultSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ for (Pair<Integer, byte[]> element : sequence) {
+ byte[] second = element.getSecond();
+ result.putInt(8 + second.length);
+ result.putInt(element.getFirst());
+ result.putInt(second.length);
+ result.put(second);
+ }
+ return result.array();
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java
new file mode 100644
index 0000000000..61652a435e
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+/**
+ * Base implementation of a supported signature for an APK.
+ */
+public class ApkSupportedSignature {
+ public final SignatureAlgorithm algorithm;
+ public final byte[] signature;
+
+ /**
+ * Constructs a new supported signature using the provided {@code algorithm} and {@code
+ * signature} bytes.
+ */
+ public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
+ this.algorithm = algorithm;
+ this.signature = signature;
+ }
+
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java
new file mode 100644
index 0000000000..b806d1e420
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.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.apk;
+
+/** APK Signature Scheme v2 content digest algorithm. */
+public enum ContentDigestAlgorithm {
+ /** SHA2-256 over 1 MB chunks. */
+ CHUNKED_SHA256(1, "SHA-256", 256 / 8),
+
+ /** SHA2-512 over 1 MB chunks. */
+ CHUNKED_SHA512(2, "SHA-512", 512 / 8),
+
+ /** SHA2-256 over 4 KB chunks for APK verity. */
+ VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
+
+ /** Non-chunk SHA2-256. */
+ SHA256(4, "SHA-256", 256 / 8);
+
+ private final int mId;
+ private final String mJcaMessageDigestAlgorithm;
+ private final int mChunkDigestOutputSizeBytes;
+
+ private ContentDigestAlgorithm(
+ int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
+ mId = id;
+ mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
+ mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
+ }
+
+ /** Returns the ID of the digest algorithm used on the APK. */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
+ * chunks by this content digest algorithm.
+ */
+ String getJcaMessageDigestAlgorithm() {
+ return mJcaMessageDigestAlgorithm;
+ }
+
+ /** Returns the size (in bytes) of the digest of a chunk of content. */
+ int getChunkDigestOutputSizeBytes() {
+ return mChunkDigestOutputSizeBytes;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java
new file mode 100644
index 0000000000..52c6085c5f
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+/**
+ * Base exception that is thrown when there are no signatures that support the full range of
+ * requested platform versions.
+ */
+public class NoApkSupportedSignaturesException extends Exception {
+ public NoApkSupportedSignaturesException(String message) {
+ super(message);
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
new file mode 100644
index 0000000000..804eb37bdd
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.Pair;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.MGF1ParameterSpec;
+import java.security.spec.PSSParameterSpec;
+
+/**
+ * APK Signing Block signature algorithm.
+ */
+public enum SignatureAlgorithm {
+ // TODO reserve the 0x0000 ID to mean null
+ /**
+ * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
+ * digested using SHA2-256 in 1 MB chunks.
+ */
+ RSA_PSS_WITH_SHA256(
+ 0x0101,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "RSA",
+ Pair.of("SHA256withRSA/PSS",
+ new PSSParameterSpec(
+ "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.M),
+
+ /**
+ * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
+ * digested using SHA2-512 in 1 MB chunks.
+ */
+ RSA_PSS_WITH_SHA512(
+ 0x0102,
+ ContentDigestAlgorithm.CHUNKED_SHA512,
+ "RSA",
+ Pair.of(
+ "SHA512withRSA/PSS",
+ new PSSParameterSpec(
+ "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.M),
+
+ /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+ RSA_PKCS1_V1_5_WITH_SHA256(
+ 0x0103,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "RSA",
+ Pair.of("SHA256withRSA", null),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.INITIAL_RELEASE),
+
+ /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
+ RSA_PKCS1_V1_5_WITH_SHA512(
+ 0x0104,
+ ContentDigestAlgorithm.CHUNKED_SHA512,
+ "RSA",
+ Pair.of("SHA512withRSA", null),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.INITIAL_RELEASE),
+
+ /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+ ECDSA_WITH_SHA256(
+ 0x0201,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "EC",
+ Pair.of("SHA256withECDSA", null),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.HONEYCOMB),
+
+ /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
+ ECDSA_WITH_SHA512(
+ 0x0202,
+ ContentDigestAlgorithm.CHUNKED_SHA512,
+ "EC",
+ Pair.of("SHA512withECDSA", null),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.HONEYCOMB),
+
+ /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
+ DSA_WITH_SHA256(
+ 0x0301,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "DSA",
+ Pair.of("SHA256withDSA", null),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.INITIAL_RELEASE),
+
+ /**
+ * DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done
+ * deterministically according to RFC 6979.
+ */
+ DETDSA_WITH_SHA256(
+ 0x0301,
+ ContentDigestAlgorithm.CHUNKED_SHA256,
+ "DSA",
+ Pair.of("SHA256withDetDSA", null),
+ AndroidSdkVersion.N,
+ AndroidSdkVersion.INITIAL_RELEASE),
+
+ /**
+ * RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
+ * the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
+ * in little endian) construct the final digest.
+ */
+ VERITY_RSA_PKCS1_V1_5_WITH_SHA256(
+ 0x0421,
+ ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
+ "RSA",
+ Pair.of("SHA256withRSA", null),
+ AndroidSdkVersion.P,
+ AndroidSdkVersion.INITIAL_RELEASE),
+
+ /**
+ * ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
+ * fsverity operates. This digest and the content length (before digestion, 8 bytes in little
+ * endian) construct the final digest.
+ */
+ VERITY_ECDSA_WITH_SHA256(
+ 0x0423,
+ ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
+ "EC",
+ Pair.of("SHA256withECDSA", null),
+ AndroidSdkVersion.P,
+ AndroidSdkVersion.HONEYCOMB),
+
+ /**
+ * DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
+ * fsverity operates. This digest and the content length (before digestion, 8 bytes in little
+ * endian) construct the final digest.
+ */
+ VERITY_DSA_WITH_SHA256(
+ 0x0425,
+ ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
+ "DSA",
+ Pair.of("SHA256withDSA", null),
+ AndroidSdkVersion.P,
+ AndroidSdkVersion.INITIAL_RELEASE);
+
+ private final int mId;
+ private final String mJcaKeyAlgorithm;
+ private final ContentDigestAlgorithm mContentDigestAlgorithm;
+ private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
+ private final int mMinSdkVersion;
+ private final int mJcaSigAlgMinSdkVersion;
+
+ SignatureAlgorithm(int id,
+ ContentDigestAlgorithm contentDigestAlgorithm,
+ String jcaKeyAlgorithm,
+ Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
+ int minSdkVersion,
+ int jcaSigAlgMinSdkVersion) {
+ mId = id;
+ mContentDigestAlgorithm = contentDigestAlgorithm;
+ mJcaKeyAlgorithm = jcaKeyAlgorithm;
+ mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
+ mMinSdkVersion = minSdkVersion;
+ mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion;
+ }
+
+ /**
+ * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the content digest algorithm associated with this signature algorithm.
+ */
+ public ContentDigestAlgorithm getContentDigestAlgorithm() {
+ return mContentDigestAlgorithm;
+ }
+
+ /**
+ * Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
+ */
+ public String getJcaKeyAlgorithm() {
+ return mJcaKeyAlgorithm;
+ }
+
+ /**
+ * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
+ * (or null if not needed) to parameterize the {@code Signature}.
+ */
+ public Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
+ return mJcaSignatureAlgAndParams;
+ }
+
+ public int getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ /**
+ * Returns the minimum SDK version that supports the JCA signature algorithm.
+ */
+ public int getJcaSigAlgMinSdkVersion() {
+ return mJcaSigAlgMinSdkVersion;
+ }
+
+ public static SignatureAlgorithm findById(int id) {
+ for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
+ if (alg.getId() == id) {
+ return alg;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java
new file mode 100644
index 0000000000..5e26327b8d
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java
new file mode 100644
index 0000000000..95f06eff8a
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk;
+
+/**
+ * Base exception that is thrown when the APK is not signed with the requested signature scheme.
+ */
+public class SignatureNotFoundException extends Exception {
+ public SignatureNotFoundException(String message) {
+ super(message);
+ }
+
+ public SignatureNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+} \ No newline at end of file
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java
new file mode 100644
index 0000000000..93627ff0e3
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */
+public class SourceStampCertificateLineage {
+
+ private final static int FIRST_VERSION = 1;
+ private final static int CURRENT_VERSION = FIRST_VERSION;
+
+ /**
+ * Deserializes the binary representation of a SourceStampCertificateLineage. Also
+ * verifies that the structure is well-formed, e.g. that the signature for each node is from its
+ * parent.
+ */
+ public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
+ throws IOException {
+ List<SigningCertificateNode> result = new ArrayList<>();
+ int nodeCount = 0;
+ if (inputBytes == null || !inputBytes.hasRemaining()) {
+ return null;
+ }
+
+ ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes);
+
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+ }
+
+ // FORMAT (little endian):
+ // * uint32: version code
+ // * sequence of length-prefixed (uint32): nodes
+ // * length-prefixed bytes: signed data
+ // * length-prefixed bytes: certificate
+ // * uint32: signature algorithm id
+ // * uint32: flags
+ // * uint32: signature algorithm id (used by to sign next cert in lineage)
+ // * length-prefixed bytes: signature over above signed data
+
+ X509Certificate lastCert = null;
+ int lastSigAlgorithmId = 0;
+
+ try {
+ int version = inputBytes.getInt();
+ if (version != CURRENT_VERSION) {
+ // we only have one version to worry about right now, so just check it
+ throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
+ + " different than any of which we are aware");
+ }
+ HashSet<X509Certificate> certHistorySet = new HashSet<>();
+ while (inputBytes.hasRemaining()) {
+ nodeCount++;
+ ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
+ ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
+ int flags = nodeBytes.getInt();
+ int sigAlgorithmId = nodeBytes.getInt();
+ SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
+ byte[] signature = readLengthPrefixedByteArray(nodeBytes);
+
+ if (lastCert != null) {
+ // Use previous level cert to verify current level
+ String jcaSignatureAlgorithm =
+ sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+ sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+ PublicKey publicKey = lastCert.getPublicKey();
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(signedData);
+ if (!sig.verify(signature)) {
+ throw new SecurityException("Unable to verify signature of certificate #"
+ + nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
+ + " SourceStampCertificateLineage object");
+ }
+ }
+
+ signedData.rewind();
+ byte[] encodedCert = readLengthPrefixedByteArray(signedData);
+ int signedSigAlgorithm = signedData.getInt();
+ if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
+ throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ + nodeBytes + " when verifying SourceStampCertificateLineage object");
+ }
+ lastCert = (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(encodedCert));
+ lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
+ if (certHistorySet.contains(lastCert)) {
+ throw new SecurityException("Encountered duplicate entries in "
+ + "SigningCertificateLineage at certificate #" + nodeCount + ". All "
+ + "signing certificates should be unique");
+ }
+ certHistorySet.add(lastCert);
+ lastSigAlgorithmId = sigAlgorithmId;
+ result.add(new SigningCertificateNode(
+ lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
+ SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
+ }
+ } catch(ApkFormatException | BufferUnderflowException e){
+ throw new IOException("Failed to parse SourceStampCertificateLineage object", e);
+ } catch(NoSuchAlgorithmException | InvalidKeyException
+ | InvalidAlgorithmParameterException | SignatureException e){
+ throw new SecurityException(
+ "Failed to verify signature over signed data for certificate #" + nodeCount
+ + " when parsing SourceStampCertificateLineage object", e);
+ } catch(CertificateException e){
+ throw new SecurityException("Failed to decode certificate #" + nodeCount
+ + " when parsing SourceStampCertificateLineage object", e);
+ }
+ return result;
+ }
+
+ /**
+ * Represents one signing certificate in the SourceStampCertificateLineage, which
+ * generally means it is/was used at some point to sign source stamps.
+ */
+ public static class SigningCertificateNode {
+
+ public SigningCertificateNode(
+ X509Certificate signingCert,
+ SignatureAlgorithm parentSigAlgorithm,
+ SignatureAlgorithm sigAlgorithm,
+ byte[] signature,
+ int flags) {
+ this.signingCert = signingCert;
+ this.parentSigAlgorithm = parentSigAlgorithm;
+ this.sigAlgorithm = sigAlgorithm;
+ this.signature = signature;
+ this.flags = flags;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SigningCertificateNode)) return false;
+
+ SigningCertificateNode that = (SigningCertificateNode) o;
+ if (!signingCert.equals(that.signingCert)) return false;
+ if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
+ if (sigAlgorithm != that.sigAlgorithm) return false;
+ if (!Arrays.equals(signature, that.signature)) return false;
+ if (flags != that.flags) return false;
+
+ // we made it
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode());
+ result = prime * result +
+ ((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode());
+ result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode());
+ result = prime * result + Arrays.hashCode(signature);
+ result = prime * result + flags;
+ return result;
+ }
+
+ /**
+ * the signing cert for this node. This is part of the data signed by the parent node.
+ */
+ public final X509Certificate signingCert;
+
+ /**
+ * the algorithm used by this node's parent to bless this data. Its ID value is part of
+ * the data signed by the parent node. {@code null} for first node.
+ */
+ public final SignatureAlgorithm parentSigAlgorithm;
+
+ /**
+ * the algorithm used by this node to bless the next node's data. Its ID value is part
+ * of the signed data of the next node. {@code null} for the last node.
+ */
+ public SignatureAlgorithm sigAlgorithm;
+
+ /**
+ * signature over the signed data (above). The signature is from this node's parent
+ * signing certificate, which should correspond to the signing certificate used to sign an
+ * APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
+ */
+ public final byte[] signature;
+
+ /**
+ * the flags detailing how the platform should treat this signing cert
+ */
+ public int flags;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
new file mode 100644
index 0000000000..2a949adb71
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+/** Constants used for source stamp signing and verification. */
+public class SourceStampConstants {
+ private SourceStampConstants() {}
+
+ public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
+ public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
+ public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
+ public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
+ /**
+ * The source stamp timestamp attribute value is an 8-byte little-endian encoded long
+ * representing the epoch time in seconds when the stamp block was signed. The first 8 bytes
+ * of the attribute value buffer will be used to read the timestamp, and any additional buffer
+ * space will be ignored.
+ */
+ public static final int STAMP_TIME_ATTR_ID = 0xe43c5946;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
new file mode 100644
index 0000000000..ef6da2f68f
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
+
+import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.Constants;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSupportedSignature;
+import com.android.apksig.internal.apk.NoApkSupportedSignaturesException;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+
+import java.io.ByteArrayInputStream;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ */
+class SourceStampVerifier {
+ /** Hidden constructor to prevent instantiation. */
+ private SourceStampVerifier() {
+ }
+
+ /**
+ * Parses the SourceStamp block and populates the {@code result}.
+ *
+ * <p>This verifies signatures over digest provided.
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the {@code [minSdkVersion,
+ * maxSdkVersion]} range.
+ */
+ public static void verifyV1SourceStamp(
+ ByteBuffer sourceStampBlockData,
+ CertificateFactory certFactory,
+ ApkSignerInfo result,
+ byte[] apkDigest,
+ byte[] sourceStampCertificateDigest,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws ApkFormatException, NoSuchAlgorithmException {
+ X509Certificate sourceStampCertificate =
+ verifySourceStampCertificate(
+ sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
+ if (result.containsWarnings() || result.containsErrors()) {
+ return;
+ }
+
+ ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData);
+ verifySourceStampSignature(
+ apkDigest,
+ minSdkVersion,
+ maxSdkVersion,
+ sourceStampCertificate,
+ apkDigestSignatures,
+ result);
+ }
+
+ /**
+ * Parses the SourceStamp block and populates the {@code result}.
+ *
+ * <p>This verifies signatures over digest of multiple signature schemes provided.
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the {@code [minSdkVersion,
+ * maxSdkVersion]} range.
+ */
+ public static void verifyV2SourceStamp(
+ ByteBuffer sourceStampBlockData,
+ CertificateFactory certFactory,
+ ApkSignerInfo result,
+ Map<Integer, byte[]> signatureSchemeApkDigests,
+ byte[] sourceStampCertificateDigest,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws ApkFormatException, NoSuchAlgorithmException {
+ X509Certificate sourceStampCertificate =
+ verifySourceStampCertificate(
+ sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
+ if (result.containsWarnings() || result.containsErrors()) {
+ return;
+ }
+
+ // Parse signed signature schemes block.
+ ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData);
+ Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
+ while (signedSignatureSchemes.hasRemaining()) {
+ ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes);
+ int signatureSchemeId = signedSignatureScheme.getInt();
+ ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme);
+ signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures);
+ }
+
+ for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
+ signatureSchemeApkDigests.entrySet()) {
+ // TODO(b/192301300): Should the new v3.1 be included in the source stamp, or since a
+ // v3.0 block must always be present with a v3.1 block is it sufficient to just use the
+ // v3.0 block?
+ if (signatureSchemeApkDigest.getKey()
+ == Constants.VERSION_APK_SIGNATURE_SCHEME_V31) {
+ continue;
+ }
+ if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
+ return;
+ }
+ verifySourceStampSignature(
+ signatureSchemeApkDigest.getValue(),
+ minSdkVersion,
+ maxSdkVersion,
+ sourceStampCertificate,
+ signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
+ result);
+ if (result.containsWarnings() || result.containsErrors()) {
+ return;
+ }
+ }
+
+ if (sourceStampBlockData.hasRemaining()) {
+ // The stamp block contains some additional attributes.
+ ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData);
+ ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData);
+
+ byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()];
+ stampAttributeData.get(stampAttributeBytes);
+ stampAttributeData.flip();
+
+ verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion,
+ sourceStampCertificate, stampAttributeDataSignatures, result);
+ if (result.containsErrors() || result.containsWarnings()) {
+ return;
+ }
+ parseStampAttributes(stampAttributeData, sourceStampCertificate, result);
+ }
+ }
+
+ private static X509Certificate verifySourceStampCertificate(
+ ByteBuffer sourceStampBlockData,
+ CertificateFactory certFactory,
+ byte[] sourceStampCertificateDigest,
+ ApkSignerInfo result)
+ throws NoSuchAlgorithmException, ApkFormatException {
+ // Parse the SourceStamp certificate.
+ byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData);
+ X509Certificate sourceStampCertificate;
+ try {
+ sourceStampCertificate = (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(sourceStampEncodedCertificate));
+ } catch (CertificateException e) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
+ return null;
+ }
+ // Wrap the cert so that the result's getEncoded returns exactly the original encoded
+ // form. Without this, getEncoded may return a different form from what was stored in
+ // the signature. This is because some X509Certificate(Factory) implementations
+ // re-encode certificates.
+ sourceStampCertificate =
+ new GuaranteedEncodedFormX509Certificate(
+ sourceStampCertificate, sourceStampEncodedCertificate);
+ result.certs.add(sourceStampCertificate);
+ // Verify the SourceStamp certificate found in the signing block is the same as the
+ // SourceStamp certificate found in the APK.
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ messageDigest.update(sourceStampEncodedCertificate);
+ byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
+ if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
+ result.addWarning(
+ ApkVerificationIssue
+ .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+ toHex(sourceStampBlockCertificateDigest),
+ toHex(sourceStampCertificateDigest));
+ return null;
+ }
+ return sourceStampCertificate;
+ }
+
+ private static void verifySourceStampSignature(
+ byte[] data,
+ int minSdkVersion,
+ int maxSdkVersion,
+ X509Certificate sourceStampCertificate,
+ ByteBuffer signatures,
+ ApkSignerInfo result) {
+ // Parse the signatures block and identify supported signatures
+ int signatureCount = 0;
+ List<ApkSupportedSignature> supportedSignatures = new ArrayList<>(1);
+ while (signatures.hasRemaining()) {
+ signatureCount++;
+ try {
+ ByteBuffer signature = getLengthPrefixedSlice(signatures);
+ int sigAlgorithmId = signature.getInt();
+ byte[] sigBytes = readLengthPrefixedByteArray(signature);
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+ if (signatureAlgorithm == null) {
+ result.addInfoMessage(
+ ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
+ sigAlgorithmId);
+ continue;
+ }
+ supportedSignatures.add(
+ new ApkSupportedSignature(signatureAlgorithm, sigBytes));
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addWarning(
+ ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
+ return;
+ }
+ }
+ if (supportedSignatures.isEmpty()) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
+ return;
+ }
+ // Verify signatures over digests using the SourceStamp's certificate.
+ List<ApkSupportedSignature> signaturesToVerify;
+ try {
+ signaturesToVerify =
+ getSignaturesToVerify(
+ supportedSignatures, minSdkVersion, maxSdkVersion, true);
+ } catch (NoApkSupportedSignaturesException e) {
+ // To facilitate debugging capture the signature algorithms and resulting exception in
+ // the warning.
+ StringBuilder signatureAlgorithms = new StringBuilder();
+ for (ApkSupportedSignature supportedSignature : supportedSignatures) {
+ if (signatureAlgorithms.length() > 0) {
+ signatureAlgorithms.append(", ");
+ }
+ signatureAlgorithms.append(supportedSignature.algorithm);
+ }
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
+ signatureAlgorithms.toString(), e);
+ return;
+ }
+ for (ApkSupportedSignature signature : signaturesToVerify) {
+ SignatureAlgorithm signatureAlgorithm = signature.algorithm;
+ String jcaSignatureAlgorithm =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+ PublicKey publicKey = sourceStampCertificate.getPublicKey();
+ try {
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(data);
+ byte[] sigBytes = signature.signature;
+ if (!sig.verify(sigBytes)) {
+ result.addWarning(
+ ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
+ return;
+ }
+ } catch (InvalidKeyException
+ | InvalidAlgorithmParameterException
+ | SignatureException
+ | NoSuchAlgorithmException e) {
+ result.addWarning(
+ ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
+ return;
+ }
+ }
+ }
+
+ private static void parseStampAttributes(ByteBuffer stampAttributeData,
+ X509Certificate sourceStampCertificate, ApkSignerInfo result)
+ throws ApkFormatException {
+ ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData);
+ int stampAttributeCount = 0;
+ while (stampAttributes.hasRemaining()) {
+ stampAttributeCount++;
+ try {
+ ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes);
+ int id = attribute.getInt();
+ byte[] value = ByteBufferUtils.toByteArray(attribute);
+ if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) {
+ readStampCertificateLineage(value, sourceStampCertificate, result);
+ } else if (id == SourceStampConstants.STAMP_TIME_ATTR_ID) {
+ long timestamp = ByteBuffer.wrap(value).order(
+ ByteOrder.LITTLE_ENDIAN).getLong();
+ if (timestamp > 0) {
+ result.timestamp = timestamp;
+ } else {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
+ timestamp);
+ }
+ } else {
+ result.addInfoMessage(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
+ }
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
+ stampAttributeCount);
+ return;
+ }
+ }
+ }
+
+ private static void readStampCertificateLineage(byte[] lineageBytes,
+ X509Certificate sourceStampCertificate, ApkSignerInfo result) {
+ try {
+ // SourceStampCertificateLineage is verified when built
+ List<SourceStampCertificateLineage.SigningCertificateNode> nodes =
+ SourceStampCertificateLineage.readSigningCertificateLineage(
+ ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN));
+ for (int i = 0; i < nodes.size(); i++) {
+ result.certificateLineage.add(nodes.get(i).signingCert);
+ }
+ // Make sure that the last cert in the chain matches this signer cert
+ if (!sourceStampCertificate.equals(
+ result.certificateLineage.get(result.certificateLineage.size() - 1))) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
+ }
+ } catch (SecurityException e) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
+ } catch (IllegalArgumentException e) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
+ } catch (Exception e) {
+ result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE);
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
new file mode 100644
index 0000000000..dee24bd1f6
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.util.Pair;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SourceStamp signer.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ *
+ * <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
+ */
+public abstract class V1SourceStampSigner {
+ public static final int V1_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
+
+ /** Hidden constructor to prevent instantiation. */
+ private V1SourceStampSigner() {}
+
+ public static Pair<byte[], Integer> generateSourceStampBlock(
+ SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
+ throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+ if (sourceStampSignerConfig.certificates.isEmpty()) {
+ throw new SignatureException("No certificates configured for signer");
+ }
+
+ List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+ for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
+ digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
+ }
+ Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+
+ SourceStampBlock sourceStampBlock = new SourceStampBlock();
+
+ try {
+ sourceStampBlock.stampCertificate =
+ sourceStampSignerConfig.certificates.get(0).getEncoded();
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException(
+ "Retrieving the encoded form of the stamp certificate failed", e);
+ }
+
+ byte[] digestBytes =
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+ sourceStampBlock.signedDigests =
+ ApkSigningBlockUtils.generateSignaturesOverData(
+ sourceStampSignerConfig, digestBytes);
+
+ // FORMAT:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
+ // * length-prefixed sequence of length-prefixed signatures:
+ // * uint32: signature algorithm ID
+ // * length-prefixed bytes: signature of signed data
+ byte[] sourceStampSignerBlock =
+ encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][] {
+ sourceStampBlock.stampCertificate,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ sourceStampBlock.signedDigests),
+ });
+
+ // FORMAT:
+ // * length-prefixed stamp block.
+ return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
+ SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID);
+ }
+
+ private static final class SourceStampBlock {
+ public byte[] stampCertificate;
+ public List<Pair<Integer, byte[]>> signedDigests;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
new file mode 100644
index 0000000000..c3fdeecc7b
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
+ */
+public abstract class V1SourceStampVerifier {
+
+ /** Hidden constructor to prevent instantiation. */
+ private V1SourceStampVerifier() {}
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+ * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
+ * {@code true}. If verification fails, the result will contain errors -- see {@link
+ * ApkSigningBlockUtils.Result#getErrors()}.
+ *
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
+ * found
+ * @throws IOException if an I/O error occurs when reading the APK
+ */
+ public static ApkSigningBlockUtils.Result verify(
+ DataSource apk,
+ ApkUtils.ZipSections zipSections,
+ byte[] sourceStampCertificateDigest,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws IOException, NoSuchAlgorithmException,
+ ApkSigningBlockUtils.SignatureNotFoundException {
+ ApkSigningBlockUtils.Result result =
+ new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(
+ apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
+
+ verify(
+ signatureInfo.signatureBlock,
+ sourceStampCertificateDigest,
+ apkContentDigests,
+ minSdkVersion,
+ maxSdkVersion,
+ result);
+ return result;
+ }
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+ * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+ * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
+ * more information about the contract of this method.
+ */
+ private static void verify(
+ ByteBuffer sourceStampBlock,
+ byte[] sourceStampCertificateDigest,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion,
+ ApkSigningBlockUtils.Result result)
+ throws NoSuchAlgorithmException {
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+ new ApkSigningBlockUtils.Result.SignerInfo();
+ result.signers.add(signerInfo);
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ ByteBuffer sourceStampBlockData =
+ ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+ byte[] digestBytes =
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ getApkDigests(apkContentDigests));
+ SourceStampVerifier.verifyV1SourceStamp(
+ sourceStampBlockData,
+ certFactory,
+ signerInfo,
+ digestBytes,
+ sourceStampCertificateDigest,
+ minSdkVersion,
+ maxSdkVersion);
+ result.verified = !result.containsErrors() && !result.containsWarnings();
+ } catch (CertificateException e) {
+ throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+ }
+
+ private static List<Pair<Integer, byte[]>> getApkDigests(
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+ List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+ for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
+ apkContentDigests.entrySet()) {
+ digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+ }
+ Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+ return digests;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
new file mode 100644
index 0000000000..9283f02673
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+
+import com.android.apksig.SigningCertificateLineage;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.util.Pair;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SourceStamp signer.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ *
+ * <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
+ */
+public class V2SourceStampSigner {
+ public static final int V2_SOURCE_STAMP_BLOCK_ID =
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
+
+ private final SignerConfig mSourceStampSignerConfig;
+ private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
+ private final boolean mSourceStampTimestampEnabled;
+
+ /** Hidden constructor to prevent instantiation. */
+ private V2SourceStampSigner(Builder builder) {
+ mSourceStampSignerConfig = builder.mSourceStampSignerConfig;
+ mSignatureSchemeDigestInfos = builder.mSignatureSchemeDigestInfos;
+ mSourceStampTimestampEnabled = builder.mSourceStampTimestampEnabled;
+ }
+
+ public static Pair<byte[], Integer> generateSourceStampBlock(
+ SignerConfig sourceStampSignerConfig,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos)
+ throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+ return new Builder(sourceStampSignerConfig,
+ signatureSchemeDigestInfos).build().generateSourceStampBlock();
+ }
+
+ public Pair<byte[], Integer> generateSourceStampBlock()
+ throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+ if (mSourceStampSignerConfig.certificates.isEmpty()) {
+ throw new SignatureException("No certificates configured for signer");
+ }
+
+ // Extract the digests for signature schemes.
+ List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>();
+ getSignedDigestsFor(
+ VERSION_APK_SIGNATURE_SCHEME_V3,
+ mSignatureSchemeDigestInfos,
+ mSourceStampSignerConfig,
+ signatureSchemeDigests);
+ getSignedDigestsFor(
+ VERSION_APK_SIGNATURE_SCHEME_V2,
+ mSignatureSchemeDigestInfos,
+ mSourceStampSignerConfig,
+ signatureSchemeDigests);
+ getSignedDigestsFor(
+ VERSION_JAR_SIGNATURE_SCHEME,
+ mSignatureSchemeDigestInfos,
+ mSourceStampSignerConfig,
+ signatureSchemeDigests);
+ Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst));
+
+ SourceStampBlock sourceStampBlock = new SourceStampBlock();
+
+ try {
+ sourceStampBlock.stampCertificate =
+ mSourceStampSignerConfig.certificates.get(0).getEncoded();
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException(
+ "Retrieving the encoded form of the stamp certificate failed", e);
+ }
+
+ sourceStampBlock.signedDigests = signatureSchemeDigests;
+
+ sourceStampBlock.stampAttributes = encodeStampAttributes(
+ generateStampAttributes(mSourceStampSignerConfig.signingCertificateLineage));
+ sourceStampBlock.signedStampAttributes =
+ ApkSigningBlockUtils.generateSignaturesOverData(mSourceStampSignerConfig,
+ sourceStampBlock.stampAttributes);
+
+ // FORMAT:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
+ // * length-prefixed sequence of length-prefixed signed signature scheme digests:
+ // * uint32: signature scheme id
+ // * length-prefixed bytes: signed digests for the respective signature scheme
+ // * length-prefixed bytes: encoded stamp attributes
+ // * length-prefixed sequence of length-prefixed signed stamp attributes:
+ // * uint32: signature algorithm id
+ // * length-prefixed bytes: signed stamp attributes for the respective signature algorithm
+ byte[] sourceStampSignerBlock =
+ encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][]{
+ sourceStampBlock.stampCertificate,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ sourceStampBlock.signedDigests),
+ sourceStampBlock.stampAttributes,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ sourceStampBlock.signedStampAttributes),
+ });
+
+ // FORMAT:
+ // * length-prefixed stamp block.
+ return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
+ SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
+ }
+
+ private static void getSignedDigestsFor(
+ int signatureSchemeVersion,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos,
+ SignerConfig mSourceStampSignerConfig,
+ List<Pair<Integer, byte[]>> signatureSchemeDigests)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ if (!mSignatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
+ return;
+ }
+
+ Map<ContentDigestAlgorithm, byte[]> digestInfo =
+ mSignatureSchemeDigestInfos.get(signatureSchemeVersion);
+ List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+ for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
+ digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
+ }
+ Collections.sort(digests, Comparator.comparing(Pair::getFirst));
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed digests:
+ // * uint32: digest algorithm id
+ // * length-prefixed bytes: digest of the respective digest algorithm
+ byte[] digestBytes =
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signed digests:
+ // * uint32: signature algorithm id
+ // * length-prefixed bytes: signed digest for the respective signature algorithm
+ List<Pair<Integer, byte[]>> signedDigest =
+ ApkSigningBlockUtils.generateSignaturesOverData(
+ mSourceStampSignerConfig, digestBytes);
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signed signature scheme digests:
+ // * uint32: signature scheme id
+ // * length-prefixed bytes: signed digests for the respective signature scheme
+ signatureSchemeDigests.add(
+ Pair.of(
+ signatureSchemeVersion,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ signedDigest)));
+ }
+
+ private static byte[] encodeStampAttributes(Map<Integer, byte[]> stampAttributes) {
+ int payloadSize = 0;
+ for (byte[] attributeValue : stampAttributes.values()) {
+ // Pair size + Attribute ID + Attribute value
+ payloadSize += 4 + 4 + attributeValue.length;
+ }
+
+ // FORMAT (little endian):
+ // * length-prefixed bytes: pair
+ // * uint32: ID
+ // * bytes: value
+ ByteBuffer result = ByteBuffer.allocate(4 + payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(payloadSize);
+ for (Map.Entry<Integer, byte[]> stampAttribute : stampAttributes.entrySet()) {
+ // Pair size
+ result.putInt(4 + stampAttribute.getValue().length);
+ result.putInt(stampAttribute.getKey());
+ result.put(stampAttribute.getValue());
+ }
+ return result.array();
+ }
+
+ private Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
+ HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
+
+ if (mSourceStampTimestampEnabled) {
+ // Write the current epoch time as the timestamp for the source stamp.
+ long timestamp = Instant.now().getEpochSecond();
+ if (timestamp > 0) {
+ ByteBuffer attributeBuffer = ByteBuffer.allocate(8);
+ attributeBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ attributeBuffer.putLong(timestamp);
+ stampAttributes.put(SourceStampConstants.STAMP_TIME_ATTR_ID,
+ attributeBuffer.array());
+ } else {
+ // The epoch time should never be <= 0, and since security decisions can potentially
+ // be made based on the value in the timestamp, throw an Exception to ensure the
+ // issues with the environment are resolved before allowing the signing.
+ throw new IllegalStateException(
+ "Received an invalid value from Instant#getTimestamp: " + timestamp);
+ }
+ }
+
+ if (lineage != null) {
+ stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID,
+ lineage.encodeSigningCertificateLineage());
+ }
+ return stampAttributes;
+ }
+
+ private static final class SourceStampBlock {
+ public byte[] stampCertificate;
+ public List<Pair<Integer, byte[]>> signedDigests;
+ // Optional stamp attributes that are not required for verification.
+ public byte[] stampAttributes;
+ public List<Pair<Integer, byte[]>> signedStampAttributes;
+ }
+
+ /** Builder of {@link V2SourceStampSigner} instances. */
+ public static class Builder {
+ private final SignerConfig mSourceStampSignerConfig;
+ private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
+ private boolean mSourceStampTimestampEnabled = true;
+
+ /**
+ * Instantiates a new {@code Builder} with the provided {@code sourceStampSignerConfig}
+ * and the {@code signatureSchemeDigestInfos}.
+ */
+ public Builder(SignerConfig sourceStampSignerConfig,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos) {
+ mSourceStampSignerConfig = sourceStampSignerConfig;
+ mSignatureSchemeDigestInfos = signatureSchemeDigestInfos;
+ }
+
+ /**
+ * Sets whether the source stamp should contain the timestamp attribute with the time
+ * at which the source stamp was signed.
+ */
+ public Builder setSourceStampTimestampEnabled(boolean value) {
+ mSourceStampTimestampEnabled = value;
+ return this;
+ }
+
+ /**
+ * Builds a new V2SourceStampSigner that can be used to generate a new source stamp
+ * block signed with the specified signing config.
+ */
+ public V2SourceStampSigner build() {
+ return new V2SourceStampSigner(this);
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
new file mode 100644
index 0000000000..a215b986a8
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.stamp;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
+
+import com.android.apksig.ApkVerificationIssue;
+import com.android.apksig.Constants;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigResult;
+import com.android.apksig.internal.apk.ApkSignerInfo;
+import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.SignatureNotFoundException;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes.
+ */
+public abstract class V2SourceStampVerifier {
+
+ /** Hidden constructor to prevent instantiation. */
+ private V2SourceStampVerifier() {}
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and returns the result of verification.
+ * The APK must be considered verified only if {@link ApkSigResult#verified} is
+ * {@code true}. If verification fails, the result will contain errors -- see {@link
+ * ApkSigResult#getErrors()}.
+ *
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ * @throws SignatureNotFoundException if no SourceStamp signatures are
+ * found
+ * @throws IOException if an I/O error occurs when reading the APK
+ */
+ public static ApkSigResult verify(
+ DataSource apk,
+ ZipSections zipSections,
+ byte[] sourceStampCertificateDigest,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+ ApkSigResult result =
+ new ApkSigResult(Constants.VERSION_SOURCE_STAMP);
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtilsLite.findSignature(
+ apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID);
+
+ verify(
+ signatureInfo.signatureBlock,
+ sourceStampCertificateDigest,
+ signatureSchemeApkContentDigests,
+ minSdkVersion,
+ maxSdkVersion,
+ result);
+ return result;
+ }
+
+ /**
+ * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
+ * {@code result}. APK is considered verified only if there are no errors reported in the {@code
+ * result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for
+ * more information about the contract of this method.
+ */
+ private static void verify(
+ ByteBuffer sourceStampBlock,
+ byte[] sourceStampCertificateDigest,
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
+ int minSdkVersion,
+ int maxSdkVersion,
+ ApkSigResult result)
+ throws NoSuchAlgorithmException {
+ ApkSignerInfo signerInfo = new ApkSignerInfo();
+ result.mSigners.add(signerInfo);
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ ByteBuffer sourceStampBlockData =
+ ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock);
+ SourceStampVerifier.verifyV2SourceStamp(
+ sourceStampBlockData,
+ certFactory,
+ signerInfo,
+ getSignatureSchemeDigests(signatureSchemeApkContentDigests),
+ sourceStampCertificateDigest,
+ minSdkVersion,
+ maxSdkVersion);
+ result.verified = !result.containsErrors() && !result.containsWarnings();
+ } catch (CertificateException e) {
+ throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+ }
+
+ private static Map<Integer, byte[]> getSignatureSchemeDigests(
+ Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) {
+ Map<Integer, byte[]> digests = new HashMap<>();
+ for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>>
+ signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) {
+ List<Pair<Integer, byte[]>> apkDigests =
+ getApkDigests(signatureSchemeApkContentDigest.getValue());
+ digests.put(
+ signatureSchemeApkContentDigest.getKey(),
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests));
+ }
+ return digests;
+ }
+
+ private static List<Pair<Integer, byte[]>> getApkDigests(
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
+ List<Pair<Integer, byte[]>> digests = new ArrayList<>();
+ for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
+ apkContentDigests.entrySet()) {
+ digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
+ }
+ Collections.sort(digests, new Comparator<Pair<Integer, byte[]>>() {
+ @Override
+ public int compare(Pair<Integer, byte[]> pair1, Pair<Integer, byte[]> pair2) {
+ return pair1.getFirst() - pair2.getFirst();
+ }
+ });
+ return digests;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java
new file mode 100644
index 0000000000..51b9810fa9
--- /dev/null
+++ b/platform/android/java/editor/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<DigestAlgorithm> BY_STRENGTH_COMPARATOR = new StrengthComparator();
+
+ private static class StrengthComparator implements Comparator<DigestAlgorithm> {
+ @Override
+ public int compare(DigestAlgorithm a1, DigestAlgorithm a2) {
+ switch (a1) {
+ case SHA1:
+ switch (a2) {
+ case SHA1:
+ return 0;
+ case SHA256:
+ return -1;
+ }
+ throw new RuntimeException("Unsupported algorithm: " + a2);
+
+ case SHA256:
+ switch (a2) {
+ case SHA1:
+ return 1;
+ case SHA256:
+ return 0;
+ }
+ throw new RuntimeException("Unsupported algorithm: " + a2);
+
+ default:
+ throw new RuntimeException("Unsupported algorithm: " + a1);
+ }
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java
new file mode 100644
index 0000000000..db1d15f618
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v1;
+
+/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */
+public class V1SchemeConstants {
+ private V1SchemeConstants() {}
+
+ public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
+ public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR =
+ "X-Android-APK-Signed";
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
new file mode 100644
index 0000000000..3cb109eff2
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v1;
+
+import static com.android.apksig.Constants.MAX_APK_SIGNERS;
+import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.asn1.Asn1EncodingException;
+import com.android.apksig.internal.jar.ManifestWriter;
+import com.android.apksig.internal.jar.SignatureFileWriter;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+import com.android.apksig.internal.util.Pair;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+/**
+ * APK signer which uses JAR signing (aka v1 signing scheme).
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
+ */
+public abstract class V1SchemeSigner {
+ public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
+
+ private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
+ new Attributes.Name("Created-By");
+ private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
+ private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
+
+ private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
+ new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
+
+ /**
+ * Signer configuration.
+ */
+ public static class SignerConfig {
+ /** Name. */
+ public String name;
+
+ /** Private key. */
+ public PrivateKey privateKey;
+
+ /**
+ * Certificates, with the first certificate containing the public key corresponding to
+ * {@link #privateKey}.
+ */
+ public List<X509Certificate> certificates;
+
+ /**
+ * Digest algorithm used for the signature.
+ */
+ public DigestAlgorithm signatureDigestAlgorithm;
+
+ /**
+ * If DSA is the signing algorithm, whether or not deterministic DSA signing should be used.
+ */
+ public boolean deterministicDsaSigning;
+ }
+
+ /** Hidden constructor to prevent instantiation. */
+ private V1SchemeSigner() {}
+
+ /**
+ * Gets the JAR signing digest algorithm to be used for signing an APK using the provided key.
+ *
+ * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
+ * AndroidManifest.xml minSdkVersion attribute)
+ *
+ * @throws InvalidKeyException if the provided key is not suitable for signing APKs using
+ * JAR signing (aka v1 signature scheme)
+ */
+ public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
+ PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
+ String keyAlgorithm = signingKey.getAlgorithm();
+ if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals((keyAlgorithm))) {
+ // Prior to API Level 18, only SHA-1 can be used with RSA.
+ if (minSdkVersion < 18) {
+ return DigestAlgorithm.SHA1;
+ }
+ return DigestAlgorithm.SHA256;
+ } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+ // Prior to API Level 21, only SHA-1 can be used with DSA
+ if (minSdkVersion < 21) {
+ return DigestAlgorithm.SHA1;
+ } else {
+ return DigestAlgorithm.SHA256;
+ }
+ } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+ if (minSdkVersion < 18) {
+ throw new InvalidKeyException(
+ "ECDSA signatures only supported for minSdkVersion 18 and higher");
+ }
+ return DigestAlgorithm.SHA256;
+ } else {
+ throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+ }
+ }
+
+ /**
+ * Returns a safe version of the provided signer name.
+ */
+ public static String getSafeSignerName(String name) {
+ if (name.isEmpty()) {
+ throw new IllegalArgumentException("Empty name");
+ }
+
+ // According to https://docs.oracle.com/javase/tutorial/deployment/jar/signing.html, the
+ // name must not be longer than 8 characters and may contain only A-Z, 0-9, _, and -.
+ StringBuilder result = new StringBuilder();
+ char[] nameCharsUpperCase = name.toUpperCase(Locale.US).toCharArray();
+ for (int i = 0; i < Math.min(nameCharsUpperCase.length, 8); i++) {
+ char c = nameCharsUpperCase[i];
+ if (((c >= 'A') && (c <= 'Z'))
+ || ((c >= '0') && (c <= '9'))
+ || (c == '-')
+ || (c == '_')) {
+ result.append(c);
+ } else {
+ result.append('_');
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
+ */
+ private static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm)
+ throws NoSuchAlgorithmException {
+ String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
+ return MessageDigest.getInstance(jcaAlgorithm);
+ }
+
+ /**
+ * Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest
+ * algorithm.
+ */
+ public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
+ return digestAlgorithm.getJcaMessageDigestAlgorithm();
+ }
+
+ /**
+ * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
+ * manifest.
+ */
+ public static boolean isJarEntryDigestNeededInManifest(String entryName) {
+ // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
+
+ // Entries which represent directories sould not be listed in the manifest.
+ if (entryName.endsWith("/")) {
+ return false;
+ }
+
+ // Entries outside of META-INF must be listed in the manifest.
+ if (!entryName.startsWith("META-INF/")) {
+ return true;
+ }
+ // Entries in subdirectories of META-INF must be listed in the manifest.
+ if (entryName.indexOf('/', "META-INF/".length()) != -1) {
+ return true;
+ }
+
+ // Ignored file names (case-insensitive) in META-INF directory:
+ // MANIFEST.MF
+ // *.SF
+ // *.RSA
+ // *.DSA
+ // *.EC
+ // SIG-*
+ String fileNameLowerCase =
+ entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
+ if (("manifest.mf".equals(fileNameLowerCase))
+ || (fileNameLowerCase.endsWith(".sf"))
+ || (fileNameLowerCase.endsWith(".rsa"))
+ || (fileNameLowerCase.endsWith(".dsa"))
+ || (fileNameLowerCase.endsWith(".ec"))
+ || (fileNameLowerCase.startsWith("sig-"))) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
+ * JAR entries which need to be added to the APK as part of the signature.
+ *
+ * @param signerConfigs signer configurations, one for each signer. At least one signer config
+ * must be provided.
+ *
+ * @throws ApkFormatException if the source manifest is malformed
+ * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
+ * missing
+ * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
+ * cannot be used in general
+ * @throws SignatureException if an error occurs when computing digests of generating
+ * signatures
+ */
+ public static List<Pair<String, byte[]>> sign(
+ List<SignerConfig> signerConfigs,
+ DigestAlgorithm jarEntryDigestAlgorithm,
+ Map<String, byte[]> jarEntryDigests,
+ List<Integer> apkSigningSchemeIds,
+ byte[] sourceManifestBytes,
+ String createdBy)
+ throws NoSuchAlgorithmException, ApkFormatException, InvalidKeyException,
+ CertificateException, SignatureException {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+ if (signerConfigs.size() > MAX_APK_SIGNERS) {
+ throw new IllegalArgumentException(
+ "APK Signature Scheme v1 only supports a maximum of " + MAX_APK_SIGNERS + ", "
+ + signerConfigs.size() + " provided");
+ }
+ OutputManifestFile manifest =
+ generateManifestFile(
+ jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
+
+ return signManifest(
+ signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, createdBy, manifest);
+ }
+
+ /**
+ * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
+ * JAR entries which need to be added to the APK as part of the signature.
+ *
+ * @param signerConfigs signer configurations, one for each signer. At least one signer config
+ * must be provided.
+ *
+ * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
+ * cannot be used in general
+ * @throws SignatureException if an error occurs when computing digests of generating
+ * signatures
+ */
+ public static List<Pair<String, byte[]>> signManifest(
+ List<SignerConfig> signerConfigs,
+ DigestAlgorithm digestAlgorithm,
+ List<Integer> apkSigningSchemeIds,
+ String createdBy,
+ OutputManifestFile manifest)
+ throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
+ SignatureException {
+ if (signerConfigs.isEmpty()) {
+ throw new IllegalArgumentException("At least one signer config must be provided");
+ }
+
+ // For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
+ List<Pair<String, byte[]>> signatureJarEntries =
+ new ArrayList<>(2 * signerConfigs.size() + 1);
+ byte[] sfBytes =
+ generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, createdBy, manifest);
+ for (SignerConfig signerConfig : signerConfigs) {
+ String signerName = signerConfig.name;
+ byte[] signatureBlock;
+ try {
+ signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
+ } catch (InvalidKeyException e) {
+ throw new InvalidKeyException(
+ "Failed to sign using signer \"" + signerName + "\"", e);
+ } catch (CertificateException e) {
+ throw new CertificateException(
+ "Failed to sign using signer \"" + signerName + "\"", e);
+ } catch (SignatureException e) {
+ throw new SignatureException(
+ "Failed to sign using signer \"" + signerName + "\"", e);
+ }
+ signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
+ PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+ String signatureBlockFileName =
+ "META-INF/" + signerName + "."
+ + publicKey.getAlgorithm().toUpperCase(Locale.US);
+ signatureJarEntries.add(
+ Pair.of(signatureBlockFileName, signatureBlock));
+ }
+ signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents));
+ return signatureJarEntries;
+ }
+
+ /**
+ * Returns the names of JAR entries which this signer will produce as part of v1 signature.
+ */
+ public static Set<String> getOutputEntryNames(List<SignerConfig> signerConfigs) {
+ Set<String> result = new HashSet<>(2 * signerConfigs.size() + 1);
+ for (SignerConfig signerConfig : signerConfigs) {
+ String signerName = signerConfig.name;
+ result.add("META-INF/" + signerName + ".SF");
+ PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+ String signatureBlockFileName =
+ "META-INF/" + signerName + "."
+ + publicKey.getAlgorithm().toUpperCase(Locale.US);
+ result.add(signatureBlockFileName);
+ }
+ result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME);
+ return result;
+ }
+
+ /**
+ * Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional)
+ * input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest.
+ */
+ public static OutputManifestFile generateManifestFile(
+ DigestAlgorithm jarEntryDigestAlgorithm,
+ Map<String, byte[]> jarEntryDigests,
+ byte[] sourceManifestBytes) throws ApkFormatException {
+ Manifest sourceManifest = null;
+ if (sourceManifestBytes != null) {
+ try {
+ sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes));
+ } catch (IOException e) {
+ throw new ApkFormatException("Malformed source META-INF/MANIFEST.MF", e);
+ }
+ }
+ ByteArrayOutputStream manifestOut = new ByteArrayOutputStream();
+ Attributes mainAttrs = new Attributes();
+ // Copy the main section from the source manifest (if provided). Otherwise use defaults.
+ // NOTE: We don't output our own Created-By header because this signer did not create the
+ // JAR/APK being signed -- the signer only adds signatures to the already existing
+ // JAR/APK.
+ if (sourceManifest != null) {
+ mainAttrs.putAll(sourceManifest.getMainAttributes());
+ } else {
+ mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION);
+ }
+
+ try {
+ ManifestWriter.writeMainSection(manifestOut, mainAttrs);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
+ }
+
+ List<String> sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet());
+ Collections.sort(sortedEntryNames);
+ SortedMap<String, byte[]> invidualSectionsContents = new TreeMap<>();
+ String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
+ for (String entryName : sortedEntryNames) {
+ checkEntryNameValid(entryName);
+ byte[] entryDigest = jarEntryDigests.get(entryName);
+ Attributes entryAttrs = new Attributes();
+ entryAttrs.putValue(
+ entryDigestAttributeName,
+ Base64.getEncoder().encodeToString(entryDigest));
+ ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
+ byte[] sectionBytes;
+ try {
+ ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
+ sectionBytes = sectionOut.toByteArray();
+ manifestOut.write(sectionBytes);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
+ }
+ invidualSectionsContents.put(entryName, sectionBytes);
+ }
+
+ OutputManifestFile result = new OutputManifestFile();
+ result.contents = manifestOut.toByteArray();
+ result.mainSectionAttributes = mainAttrs;
+ result.individualSectionsContents = invidualSectionsContents;
+ return result;
+ }
+
+ private static void checkEntryNameValid(String name) throws ApkFormatException {
+ // JAR signing spec says CR, LF, and NUL are not permitted in entry names
+ // CR or LF in entry names will result in malformed MANIFEST.MF and .SF files because there
+ // is no way to escape characters in MANIFEST.MF and .SF files. NUL can, presumably, cause
+ // issues when parsing using C and C++ like languages.
+ for (char c : name.toCharArray()) {
+ if ((c == '\r') || (c == '\n') || (c == 0)) {
+ throw new ApkFormatException(
+ String.format(
+ "Unsupported character 0x%1$02x in ZIP entry name \"%2$s\"",
+ (int) c,
+ name));
+ }
+ }
+ }
+
+ public static class OutputManifestFile {
+ public byte[] contents;
+ public SortedMap<String, byte[]> individualSectionsContents;
+ public Attributes mainSectionAttributes;
+ }
+
+ private static byte[] generateSignatureFile(
+ List<Integer> apkSignatureSchemeIds,
+ DigestAlgorithm manifestDigestAlgorithm,
+ String createdBy,
+ OutputManifestFile manifest) throws NoSuchAlgorithmException {
+ Manifest sf = new Manifest();
+ Attributes mainAttrs = sf.getMainAttributes();
+ mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION);
+ mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, createdBy);
+ if (!apkSignatureSchemeIds.isEmpty()) {
+ // Add APK Signature Scheme v2 (and newer) signature stripping protection.
+ // This attribute indicates that this APK is supposed to have been signed using one or
+ // more APK-specific signature schemes in addition to the standard JAR signature scheme
+ // used by this code. APK signature verifier should reject the APK if it does not
+ // contain a signature for the signature scheme the verifier prefers out of this set.
+ StringBuilder attrValue = new StringBuilder();
+ for (int id : apkSignatureSchemeIds) {
+ if (attrValue.length() > 0) {
+ attrValue.append(", ");
+ }
+ attrValue.append(String.valueOf(id));
+ }
+ mainAttrs.put(
+ SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME,
+ attrValue.toString());
+ }
+
+ // Add main attribute containing the digest of MANIFEST.MF.
+ MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
+ mainAttrs.putValue(
+ getManifestDigestAttributeName(manifestDigestAlgorithm),
+ Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ SignatureFileWriter.writeMainSection(out, mainAttrs);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write in-memory .SF file", e);
+ }
+ String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm);
+ for (Map.Entry<String, byte[]> manifestSection
+ : manifest.individualSectionsContents.entrySet()) {
+ String sectionName = manifestSection.getKey();
+ byte[] sectionContents = manifestSection.getValue();
+ byte[] sectionDigest = md.digest(sectionContents);
+ Attributes attrs = new Attributes();
+ attrs.putValue(
+ entryDigestAttributeName,
+ Base64.getEncoder().encodeToString(sectionDigest));
+
+ try {
+ SignatureFileWriter.writeIndividualSection(out, sectionName, attrs);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write in-memory .SF file", e);
+ }
+ }
+
+ // A bug in the java.util.jar implementation of Android platforms up to version 1.6 will
+ // cause a spurious IOException to be thrown if the length of the signature file is a
+ // multiple of 1024 bytes. As a workaround, add an extra CRLF in this case.
+ if ((out.size() > 0) && ((out.size() % 1024) == 0)) {
+ try {
+ SignatureFileWriter.writeSectionDelimiter(out);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write to ByteArrayOutputStream", e);
+ }
+ }
+
+ return out.toByteArray();
+ }
+
+
+
+ /**
+ * Generates the CMS PKCS #7 signature block corresponding to the provided signature file and
+ * signing configuration.
+ */
+ private static byte[] generateSignatureBlock(
+ SignerConfig signerConfig, byte[] signatureFileBytes)
+ throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
+ SignatureException {
+ // Obtain relevant bits of signing configuration
+ List<X509Certificate> signerCerts = signerConfig.certificates;
+ X509Certificate signingCert = signerCerts.get(0);
+ PublicKey publicKey = signingCert.getPublicKey();
+ DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
+ Pair<String, AlgorithmIdentifier> signatureAlgs =
+ getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm,
+ signerConfig.deterministicDsaSigning);
+ String jcaSignatureAlgorithm = signatureAlgs.getFirst();
+
+ // Generate the cryptographic signature of the signature file
+ byte[] signatureBytes;
+ try {
+ Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
+ signature.initSign(signerConfig.privateKey);
+ signature.update(signatureFileBytes);
+ signatureBytes = signature.sign();
+ } catch (InvalidKeyException e) {
+ throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
+ } catch (SignatureException e) {
+ throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
+ }
+
+ // Verify the signature against the public key in the signing certificate
+ try {
+ Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
+ signature.initVerify(publicKey);
+ signature.update(signatureFileBytes);
+ if (!signature.verify(signatureBytes)) {
+ throw new SignatureException("Signature did not verify");
+ }
+ } catch (InvalidKeyException e) {
+ throw new InvalidKeyException(
+ "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+ + " public key from certificate",
+ e);
+ } catch (SignatureException e) {
+ throw new SignatureException(
+ "Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+ + " public key from certificate",
+ e);
+ }
+
+ AlgorithmIdentifier digestAlgorithmId =
+ getSignerInfoDigestAlgorithmOid(digestAlgorithm);
+ AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
+ try {
+ return ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
+ signatureBytes,
+ null,
+ signerCerts, digestAlgorithmId,
+ signatureAlgorithmId);
+ } catch (Asn1EncodingException | CertificateEncodingException ex) {
+ throw new SignatureException("Failed to encode signature block");
+ }
+ }
+
+
+
+ private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case SHA1:
+ return "SHA1-Digest";
+ case SHA256:
+ return "SHA-256-Digest";
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected content digest algorithm: " + digestAlgorithm);
+ }
+ }
+
+ private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case SHA1:
+ return "SHA1-Digest-Manifest";
+ case SHA256:
+ return "SHA-256-Digest-Manifest";
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected content digest algorithm: " + digestAlgorithm);
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
new file mode 100644
index 0000000000..f3fd64156f
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
@@ -0,0 +1,1570 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v1;
+
+import static com.android.apksig.Constants.MAX_APK_SIGNERS;
+import static com.android.apksig.internal.oid.OidConstants.getSigAlgSupportedApiLevels;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaDigestAlgorithm;
+import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaSignatureAlgorithm;
+import static com.android.apksig.internal.x509.Certificate.findCertificate;
+import static com.android.apksig.internal.x509.Certificate.parseCertificates;
+
+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.apk.ApkSigningBlockUtils;
+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.oid.OidConstants;
+import com.android.apksig.internal.pkcs7.Attribute;
+import com.android.apksig.internal.pkcs7.ContentInfo;
+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.SignerInfo;
+import com.android.apksig.internal.util.AndroidSdkVersion;
+import com.android.apksig.internal.util.ByteBufferUtils;
+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.internal.zip.ZipUtils;
+import com.android.apksig.util.DataSinks;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+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;
+
+/**
+ * APK verifier which uses JAR signing (aka v1 signing scheme).
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
+ */
+public abstract class V1SchemeVerifier {
+ 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()}.
+ *
+ * <p>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<Integer, String> supportedApkSigSchemeNames,
+ Set<Integer> 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<CentralDirectoryRecord> cdRecords = parseZipCentralDirectory(apk, apkSections);
+ Set<String> 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<String> checkForDuplicateEntries(
+ List<CentralDirectoryRecord> cdRecords, Result result) {
+ Set<String> cdEntryNames = new HashSet<>(cdRecords.size());
+ Set<String> 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<ManifestParser.Section, Map<String, ManifestParser.Section>> parseManifest(
+ byte[] manifestBytes, Set<String> cdEntryNames, Result result) {
+ ManifestParser manifest = new ManifestParser(manifestBytes);
+ ManifestParser.Section manifestMainSection = manifest.readSection();
+ List<ManifestParser.Section> manifestIndividualSections = manifest.readAllSections();
+ Map<String, ManifestParser.Section> 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<CentralDirectoryRecord> cdRecords,
+ Set<String> cdEntryNames,
+ Map<Integer, String> supportedApkSigSchemeNames,
+ Set<Integer> foundApkSigSchemeIds,
+ int minSdkVersion,
+ int maxSdkVersion,
+ Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException {
+
+ // Find JAR manifest and signature block files.
+ CentralDirectoryRecord manifestEntry = null;
+ Map<String, CentralDirectoryRecord> sigFileEntries = new HashMap<>(1);
+ List<CentralDirectoryRecord> sigBlockEntries = new ArrayList<>(1);
+ for (CentralDirectoryRecord cdRecord : cdRecords) {
+ String entryName = cdRecord.getName();
+ if (!entryName.startsWith("META-INF/")) {
+ continue;
+ }
+ if ((manifestEntry == null) && (V1SchemeConstants.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<ManifestParser.Section, Map<String, ManifestParser.Section>> manifestSections =
+ parseManifest(manifestBytes, cdEntryNames, result);
+
+ if (result.containsErrors()) {
+ return;
+ }
+
+ ManifestParser.Section manifestMainSection = manifestSections.getFirst();
+ Map<String, ManifestParser.Section> entryNameToManifestSection =
+ manifestSections.getSecond();
+
+ // STATE OF AFFAIRS:
+ // * All JAR entries listed in JAR manifest are present in the APK.
+
+ // Identify signers
+ List<Signer> 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;
+ }
+ if (signers.size() > MAX_APK_SIGNERS) {
+ result.addError(Issue.JAR_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS,
+ signers.size());
+ 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<Signer> 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<Signer> 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<String> 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<String> 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<String> 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<SignerInfo> unverifiedSignerInfosToTry;
+ if (minSdkVersion < AndroidSdkVersion.N) {
+ unverifiedSignerInfosToTry =
+ Collections.singletonList(signedData.signerInfos.get(0));
+ } else {
+ unverifiedSignerInfosToTry = signedData.signerInfos;
+ }
+ List<X509Certificate> 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<X509Certificate> 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<X509Certificate> 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<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported =
+ getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid);
+ List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported =
+ desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported);
+ if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) {
+ String digestAlgorithmUserFriendly =
+ OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
+ digestAlgorithmOid);
+ if (digestAlgorithmUserFriendly == null) {
+ digestAlgorithmUserFriendly = digestAlgorithmOid;
+ }
+ String signatureAlgorithmUserFriendly =
+ OidConstants.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);
+ PublicKey publicKey = signingCertificate.getPublicKey();
+ try {
+ s.initVerify(publicKey);
+ } catch (InvalidKeyException e) {
+ // An InvalidKeyException could be caught if the PublicKey in the certificate is not
+ // properly encoded; attempt to resolve any encoding errors, generate a new public
+ // key, and reattempt the initVerify with the newly encoded key.
+ try {
+ byte[] encodedPublicKey = ApkSigningBlockUtils.encodePublicKey(publicKey);
+ publicKey = KeyFactory.getInstance(publicKey.getAlgorithm()).generatePublic(
+ new X509EncodedKeySpec(encodedPublicKey));
+ } catch (InvalidKeySpecException ikse) {
+ // If an InvalidKeySpecException is caught then throw the original Exception
+ // since the key couldn't be properly re-encoded, and the original Exception
+ // will have more useful debugging info.
+ throw e;
+ }
+ s = Signature.getInstance(jcaSignatureAlgorithm);
+ s.initVerify(publicKey);
+ }
+
+ 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<Attribute> 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;
+ }
+
+
+
+ public static List<X509Certificate> getCertificateChain(
+ List<X509Certificate> certs, X509Certificate leaf) {
+ List<X509Certificate> unusedCerts = new ArrayList<>(certs);
+ List<X509Certificate> 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;
+ }
+
+
+
+
+ public void verifySigFileAgainstManifest(
+ byte[] manifestBytes,
+ ManifestParser.Section manifestMainSection,
+ Map<String, ManifestParser.Section> entryNameToManifestSection,
+ Map<Integer, String> supportedApkSigSchemeNames,
+ Set<Integer> 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<ManifestParser.Section> sfSections = sf.readAllSections();
+ Set<String> 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<NamedDigest> 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,
+ V1SchemeConstants.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<NamedDigest> 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<NamedDigest> 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<Integer, String> supportedApkSigSchemeNames,
+ Set<Integer> foundApkSigSchemeIds) {
+ String signedWithApkSchemes =
+ sfMainSection.getAttributeValue(
+ V1SchemeConstants.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<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
+ Set<Integer> 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<NamedDigest> getDigestsToVerify(
+ ManifestParser.Section section,
+ String digestAttrSuffix,
+ int minSdkVersion,
+ int maxSdkVersion) {
+ Decoder base64Decoder = Base64.getDecoder();
+ List<NamedDigest> 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<String, String> 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<String, Integer>
+ 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<NamedDigest> digests, String jcaDigestAlgorithm) {
+ for (NamedDigest digest : digests) {
+ if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) {
+ return digest.digest;
+ }
+ }
+ return null;
+ }
+
+ public static List<CentralDirectoryRecord> parseZipCentralDirectory(
+ DataSource apk,
+ ApkUtils.ZipSections apkSections)
+ throws IOException, ApkFormatException {
+ return ZipUtils.parseZipCentralDirectory(apk, apkSections);
+ }
+
+ /**
+ * 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<Signer> verifyJarEntriesAgainstManifestAndSigners(
+ DataSource apk,
+ long cdOffsetInApk,
+ Collection<CentralDirectoryRecord> cdRecords,
+ Map<String, ManifestParser.Section> entryNameToManifestSection,
+ List<Signer> signers,
+ int minSdkVersion,
+ int maxSdkVersion,
+ Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException {
+ // Iterate over APK contents as sequentially as possible to improve performance.
+ List<CentralDirectoryRecord> cdRecordsSortedByLocalFileHeaderOffset =
+ new ArrayList<>(cdRecords);
+ Collections.sort(
+ cdRecordsSortedByLocalFileHeaderOffset,
+ CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
+ List<Signer> firstSignedEntrySigners = null;
+ String firstSignedEntryName = null;
+ for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) {
+ String entryName = cdRecord.getName();
+ 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<Signer> 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<NamedDigest> 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,
+ V1SchemeConstants.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<String> getSignerNames(List<Signer> signers) {
+ if (signers.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<String> 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<SignerInfo> 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<SignerInfo> ignoredSigners = new ArrayList<>();
+
+ private final List<IssueWithParams> mWarnings = new ArrayList<>();
+ private final List<IssueWithParams> 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<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+
+ public static class SignerInfo {
+ public final String name;
+ public final String signatureFileName;
+ public final String signatureBlockFileName;
+ public final List<X509Certificate> certChain = new ArrayList<>();
+
+ private final List<IssueWithParams> mWarnings = new ArrayList<>();
+ private final List<IssueWithParams> 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<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+ }
+ }
+
+ private static class SignedAttributes {
+ private Map<String, List<Asn1OpaqueObject>> mAttrs;
+
+ public SignedAttributes(Collection<Attribute> attrs) throws Pkcs7DecodingException {
+ Map<String, List<Asn1OpaqueObject>> 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<Asn1OpaqueObject> 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/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java
new file mode 100644
index 0000000000..0e244c8373
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v2;
+
+/** Constants used by the V2 Signature Scheme signing and verification. */
+public class V2SchemeConstants {
+ private V2SchemeConstants() {}
+
+ public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+ public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
new file mode 100644
index 0000000000..06da96cf20
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java
@@ -0,0 +1,329 @@
+/*
+ * 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.Constants.MAX_APK_SIGNERS;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
+
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.interfaces.ECKey;
+import java.security.interfaces.RSAKey;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APK Signature Scheme v2 signer.
+ *
+ * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
+ * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
+ * uncompressed contents of ZIP entries.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ */
+public abstract class V2SchemeSigner {
+ /*
+ * The two main goals of APK Signature Scheme v2 are:
+ * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
+ * cover every byte of the APK being signed.
+ * 2. Enable much faster signature and integrity verification. This is achieved by requiring
+ * only a minimal amount of APK parsing before the signature is verified, thus completely
+ * bypassing ZIP entry decompression and by making integrity verification parallelizable by
+ * employing a hash tree.
+ *
+ * The generated signature block is wrapped into an APK Signing Block and inserted into the
+ * original APK immediately before the start of ZIP Central Directory. This is to ensure that
+ * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
+ * extensibility. For example, a future signature scheme could insert its signatures there as
+ * well. The contract of the APK Signing Block is that all contents outside of the block must be
+ * protected by signatures inside the block.
+ */
+
+ public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+
+ /** Hidden constructor to prevent instantiation. */
+ private V2SchemeSigner() {}
+
+ /**
+ * Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
+ * provided key.
+ *
+ * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
+ * AndroidManifest.xml minSdkVersion attribute).
+ * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
+ * Signature Scheme v2
+ */
+ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+ int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
+ throws InvalidKeyException {
+ String keyAlgorithm = signingKey.getAlgorithm();
+ if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+ // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
+ // deterministic signatures which make life easier for OTA updates (fewer files
+ // changed when deterministic signature schemes are used).
+
+ // Pick a digest which is no weaker than the key.
+ int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
+ if (modulusLengthBits <= 3072) {
+ // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
+ List<SignatureAlgorithm> algorithms = new ArrayList<>();
+ algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
+ if (verityEnabled) {
+ algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
+ }
+ return algorithms;
+ } else {
+ // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
+ // digest being the weak link. SHA-512 is the next strongest supported digest.
+ return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
+ }
+ } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+ // DSA is supported only with SHA-256.
+ List<SignatureAlgorithm> algorithms = new ArrayList<>();
+ algorithms.add(
+ deterministicDsaSigning ?
+ SignatureAlgorithm.DETDSA_WITH_SHA256 :
+ SignatureAlgorithm.DSA_WITH_SHA256);
+ if (verityEnabled) {
+ algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
+ }
+ return algorithms;
+ } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+ // Pick a digest which is no weaker than the key.
+ int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
+ if (keySizeBits <= 256) {
+ // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
+ List<SignatureAlgorithm> algorithms = new ArrayList<>();
+ algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
+ if (verityEnabled) {
+ algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
+ }
+ return algorithms;
+ } else {
+ // Keys longer than 256 bit need to be paired with a stronger digest to avoid the
+ // digest being the weak link. SHA-512 is the next strongest supported digest.
+ return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
+ }
+ } else {
+ throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+ }
+ }
+
+ public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+ generateApkSignatureSchemeV2Block(RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs,
+ boolean v3SigningEnabled)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd,
+ signerConfigs, v3SigningEnabled, null);
+ }
+
+ public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
+ generateApkSignatureSchemeV2Block(
+ RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs,
+ boolean v3SigningEnabled,
+ List<byte[]> preservedV2SignerBlocks)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException {
+ Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
+ ApkSigningBlockUtils.computeContentDigests(
+ executor, beforeCentralDir, centralDir, eocd, signerConfigs);
+ return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
+ generateApkSignatureSchemeV2Block(
+ digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled,
+ preservedV2SignerBlocks),
+ digestInfo.getSecond());
+ }
+
+ private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
+ List<SignerConfig> signerConfigs,
+ Map<ContentDigestAlgorithm, byte[]> contentDigests,
+ boolean v3SigningEnabled,
+ List<byte[]> preservedV2SignerBlocks)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signer blocks.
+
+ if (signerConfigs.size() > MAX_APK_SIGNERS) {
+ throw new IllegalArgumentException(
+ "APK Signature Scheme v2 only supports a maximum of " + MAX_APK_SIGNERS + ", "
+ + signerConfigs.size() + " provided");
+ }
+
+ List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
+ if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) {
+ signerBlocks.addAll(preservedV2SignerBlocks);
+ }
+ int signerNumber = 0;
+ for (SignerConfig signerConfig : signerConfigs) {
+ signerNumber++;
+ byte[] signerBlock;
+ try {
+ signerBlock = generateSignerBlock(signerConfig, contentDigests, v3SigningEnabled);
+ } catch (InvalidKeyException e) {
+ throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
+ } catch (SignatureException e) {
+ throw new SignatureException("Signer #" + signerNumber + " failed", e);
+ }
+ signerBlocks.add(signerBlock);
+ }
+
+ return Pair.of(
+ encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][] {
+ encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
+ }),
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+ }
+
+ private static byte[] generateSignerBlock(
+ SignerConfig signerConfig,
+ Map<ContentDigestAlgorithm, byte[]> contentDigests,
+ boolean v3SigningEnabled)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ if (signerConfig.certificates.isEmpty()) {
+ throw new SignatureException("No certificates configured for signer");
+ }
+ PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+
+ byte[] encodedPublicKey = encodePublicKey(publicKey);
+
+ V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
+ try {
+ signedData.certificates = encodeCertificates(signerConfig.certificates);
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException("Failed to encode certificates", e);
+ }
+
+ List<Pair<Integer, byte[]>> digests =
+ new ArrayList<>(signerConfig.signatureAlgorithms.size());
+ for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+ ContentDigestAlgorithm contentDigestAlgorithm =
+ signatureAlgorithm.getContentDigestAlgorithm();
+ byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
+ if (contentDigest == null) {
+ throw new RuntimeException(
+ contentDigestAlgorithm
+ + " content digest for "
+ + signatureAlgorithm
+ + " not computed");
+ }
+ digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
+ }
+ signedData.digests = digests;
+ signedData.additionalAttributes = generateAdditionalAttributes(v3SigningEnabled);
+
+ V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed digests:
+ // * uint32: signature algorithm ID
+ // * length-prefixed bytes: digest of contents
+ // * length-prefixed sequence of certificates:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+ // * length-prefixed sequence of length-prefixed additional attributes:
+ // * uint32: ID
+ // * (length - 4) bytes: value
+
+ signer.signedData =
+ encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][] {
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ signedData.digests),
+ encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
+ signedData.additionalAttributes,
+ new byte[0],
+ });
+ signer.publicKey = encodedPublicKey;
+ signer.signatures = new ArrayList<>();
+ signer.signatures =
+ ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
+
+ // FORMAT:
+ // * length-prefixed signed data
+ // * length-prefixed sequence of length-prefixed signatures:
+ // * uint32: signature algorithm ID
+ // * length-prefixed bytes: signature of signed data
+ // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
+ return encodeAsSequenceOfLengthPrefixedElements(
+ new byte[][] {
+ signer.signedData,
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ signer.signatures),
+ signer.publicKey,
+ });
+ }
+
+ private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) {
+ if (v3SigningEnabled) {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: attribute pair
+ // * uint32: ID - STRIPPING_PROTECTION_ATTR_ID in this case
+ // * uint32: value - 3 (v3 signature scheme id) in this case
+ int payloadSize = 4 + 4 + 4;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(payloadSize - 4);
+ result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID);
+ result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
+ return result.array();
+ } else {
+ return new byte[0];
+ }
+ }
+
+ private static final class V2SignatureSchemeBlock {
+ private static final class Signer {
+ public byte[] signedData;
+ public List<Pair<Integer, byte[]>> signatures;
+ public byte[] publicKey;
+ }
+
+ private static final class SignedData {
+ public List<Pair<Integer, byte[]>> digests;
+ public List<byte[]> certificates;
+ public byte[] additionalAttributes;
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
new file mode 100644
index 0000000000..4d6e3e1a8c
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
@@ -0,0 +1,471 @@
+/*
+ * 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.Constants.MAX_APK_SIGNERS;
+
+import com.android.apksig.ApkVerifier.Issue;
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * APK Signature Scheme v2 verifier.
+ *
+ * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
+ * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
+ * uncompressed contents of ZIP entries.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ */
+public abstract class V2SchemeVerifier {
+ /** Hidden constructor to prevent instantiation. */
+ private V2SchemeVerifier() {}
+
+ /**
+ * Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of
+ * verification. The APK must be considered verified only if
+ * {@link ApkSigningBlockUtils.Result#verified} is
+ * {@code true}. If verification fails, the result will contain errors -- see
+ * {@link ApkSigningBlockUtils.Result#getErrors()}.
+ *
+ * <p>Verification succeeds iff the APK's APK Signature Scheme v2 signatures are expected to
+ * verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
+ * If the APK's signature is expected to not verify on any of the specified platform versions,
+ * this method returns a result with one or more errors and whose
+ * {@code Result.verified == false}, or this method throws an exception.
+ *
+ * @throws ApkFormatException if the APK is malformed
+ * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
+ * required cryptographic algorithm implementation is missing
+ * @throws ApkSigningBlockUtils.SignatureNotFoundException if no APK Signature Scheme v2
+ * signatures are found
+ * @throws IOException if an I/O error occurs when reading the APK
+ */
+ public static ApkSigningBlockUtils.Result verify(
+ RunnablesExecutor executor,
+ DataSource apk,
+ ApkUtils.ZipSections zipSections,
+ Map<Integer, String> supportedApkSigSchemeNames,
+ Set<Integer> foundSigSchemeIds,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws IOException, ApkFormatException, NoSuchAlgorithmException,
+ ApkSigningBlockUtils.SignatureNotFoundException {
+ ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result);
+
+ DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
+ DataSource centralDir =
+ apk.slice(
+ signatureInfo.centralDirOffset,
+ signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
+ ByteBuffer eocd = signatureInfo.eocd;
+
+ verify(executor,
+ beforeApkSigningBlock,
+ signatureInfo.signatureBlock,
+ centralDir,
+ eocd,
+ supportedApkSigSchemeNames,
+ foundSigSchemeIds,
+ minSdkVersion,
+ maxSdkVersion,
+ result);
+ return result;
+ }
+
+ /**
+ * Verifies the provided APK's v2 signatures and outputs the results into the provided
+ * {@code result}. APK is considered verified only if there are no errors reported in the
+ * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, Map,
+ * Set, int, int)} for more information about the contract of this method.
+ *
+ * @param result result populated by this method with interesting information about the APK,
+ * such as information about signers, and verification errors and warnings.
+ */
+ private static void verify(
+ RunnablesExecutor executor,
+ DataSource beforeApkSigningBlock,
+ ByteBuffer apkSignatureSchemeV2Block,
+ DataSource centralDir,
+ ByteBuffer eocd,
+ Map<Integer, String> supportedApkSigSchemeNames,
+ Set<Integer> foundSigSchemeIds,
+ int minSdkVersion,
+ int maxSdkVersion,
+ ApkSigningBlockUtils.Result result)
+ throws IOException, NoSuchAlgorithmException {
+ Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+ parseSigners(
+ apkSignatureSchemeV2Block,
+ contentDigestsToVerify,
+ supportedApkSigSchemeNames,
+ foundSigSchemeIds,
+ minSdkVersion,
+ maxSdkVersion,
+ result);
+ if (result.containsErrors()) {
+ return;
+ }
+ ApkSigningBlockUtils.verifyIntegrity(
+ executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
+ if (!result.containsErrors()) {
+ result.verified = true;
+ }
+ }
+
+ /**
+ * Parses each signer in the provided APK Signature Scheme v2 block and populates corresponding
+ * {@code signerInfos} of the provided {@code result}.
+ *
+ * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
+ * However, this does not verify the integrity of the rest of the APK but rather simply reports
+ * the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the
+ * {@code [minSdkVersion, maxSdkVersion]} range.
+ */
+ public static void parseSigners(
+ ByteBuffer apkSignatureSchemeV2Block,
+ Set<ContentDigestAlgorithm> contentDigestsToVerify,
+ Map<Integer, String> supportedApkSigSchemeNames,
+ Set<Integer> foundApkSigSchemeIds,
+ int minSdkVersion,
+ int maxSdkVersion,
+ ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
+ ByteBuffer signers;
+ try {
+ signers = ApkSigningBlockUtils.getLengthPrefixedSlice(apkSignatureSchemeV2Block);
+ } catch (ApkFormatException e) {
+ result.addError(Issue.V2_SIG_MALFORMED_SIGNERS);
+ return;
+ }
+ if (!signers.hasRemaining()) {
+ result.addError(Issue.V2_SIG_NO_SIGNERS);
+ return;
+ }
+
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+ int signerCount = 0;
+ while (signers.hasRemaining()) {
+ int signerIndex = signerCount;
+ signerCount++;
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo =
+ new ApkSigningBlockUtils.Result.SignerInfo();
+ signerInfo.index = signerIndex;
+ result.signers.add(signerInfo);
+ try {
+ ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers);
+ parseSigner(
+ signer,
+ certFactory,
+ signerInfo,
+ contentDigestsToVerify,
+ supportedApkSigSchemeNames,
+ foundApkSigSchemeIds,
+ minSdkVersion,
+ maxSdkVersion);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER);
+ return;
+ }
+ }
+ if (signerCount > MAX_APK_SIGNERS) {
+ result.addError(Issue.V2_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS, signerCount);
+ }
+ }
+
+ /**
+ * Parses the provided signer block and populates the {@code result}.
+ *
+ * <p>This verifies signatures over {@code signed-data} contained in this block but does not
+ * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
+ * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
+ * integrity of the APK.
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the
+ * {@code [minSdkVersion, maxSdkVersion]} range.
+ */
+ private static void parseSigner(
+ ByteBuffer signerBlock,
+ CertificateFactory certFactory,
+ ApkSigningBlockUtils.Result.SignerInfo result,
+ Set<ContentDigestAlgorithm> contentDigestsToVerify,
+ Map<Integer, String> supportedApkSigSchemeNames,
+ Set<Integer> foundApkSigSchemeIds,
+ int minSdkVersion,
+ int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException {
+ ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
+ byte[] signedDataBytes = new byte[signedData.remaining()];
+ signedData.get(signedDataBytes);
+ signedData.flip();
+ result.signedData = signedDataBytes;
+
+ ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
+ byte[] publicKeyBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signerBlock);
+
+ // Parse the signatures block and identify supported signatures
+ int signatureCount = 0;
+ List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
+ while (signatures.hasRemaining()) {
+ signatureCount++;
+ try {
+ ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
+ int sigAlgorithmId = signature.getInt();
+ byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
+ result.signatures.add(
+ new ApkSigningBlockUtils.Result.SignerInfo.Signature(
+ sigAlgorithmId, sigBytes));
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+ if (signatureAlgorithm == null) {
+ result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+ continue;
+ }
+ supportedSignatures.add(
+ new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount);
+ return;
+ }
+ }
+ if (result.signatures.isEmpty()) {
+ result.addError(Issue.V2_SIG_NO_SIGNATURES);
+ return;
+ }
+
+ // Verify signatures over signed-data block using the public key
+ List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
+ try {
+ signaturesToVerify =
+ ApkSigningBlockUtils.getSignaturesToVerify(
+ supportedSignatures, minSdkVersion, maxSdkVersion);
+ } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
+ result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES, e);
+ return;
+ }
+ for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
+ SignatureAlgorithm signatureAlgorithm = signature.algorithm;
+ String jcaSignatureAlgorithm =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+ String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
+ PublicKey publicKey;
+ try {
+ publicKey =
+ KeyFactory.getInstance(keyAlgorithm).generatePublic(
+ new X509EncodedKeySpec(publicKeyBytes));
+ } catch (Exception e) {
+ result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e);
+ return;
+ }
+ try {
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ signedData.position(0);
+ sig.update(signedData);
+ byte[] sigBytes = signature.signature;
+ if (!sig.verify(sigBytes)) {
+ result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm);
+ return;
+ }
+ result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+ contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException
+ | SignatureException e) {
+ result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
+ return;
+ }
+ }
+
+ // At least one signature over signedData has verified. We can now parse signed-data.
+ signedData.position(0);
+ ByteBuffer digests = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
+ ByteBuffer certificates = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
+ ByteBuffer additionalAttributes = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
+
+ // Parse the certificates block
+ int certificateIndex = -1;
+ while (certificates.hasRemaining()) {
+ certificateIndex++;
+ byte[] encodedCert = ApkSigningBlockUtils.readLengthPrefixedByteArray(certificates);
+ X509Certificate certificate;
+ try {
+ certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
+ } catch (CertificateException e) {
+ result.addError(
+ Issue.V2_SIG_MALFORMED_CERTIFICATE,
+ certificateIndex,
+ certificateIndex + 1,
+ e);
+ return;
+ }
+ // Wrap the cert so that the result's getEncoded returns exactly the original encoded
+ // form. Without this, getEncoded may return a different form from what was stored in
+ // the signature. This is because some X509Certificate(Factory) implementations
+ // re-encode certificates.
+ certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
+ result.certs.add(certificate);
+ }
+
+ if (result.certs.isEmpty()) {
+ result.addError(Issue.V2_SIG_NO_CERTIFICATES);
+ return;
+ }
+ X509Certificate mainCertificate = result.certs.get(0);
+ byte[] certificatePublicKeyBytes;
+ try {
+ certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
+ mainCertificate.getPublicKey());
+ } catch (InvalidKeyException e) {
+ System.out.println("Caught an exception encoding the public key: " + e);
+ e.printStackTrace();
+ certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
+ }
+ if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+ result.addError(
+ Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+ ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
+ ApkSigningBlockUtils.toHex(publicKeyBytes));
+ return;
+ }
+
+ // Parse the digests block
+ int digestCount = 0;
+ while (digests.hasRemaining()) {
+ digestCount++;
+ try {
+ ByteBuffer digest = ApkSigningBlockUtils.getLengthPrefixedSlice(digests);
+ int sigAlgorithmId = digest.getInt();
+ byte[] digestBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(digest);
+ result.contentDigests.add(
+ new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
+ sigAlgorithmId, digestBytes));
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
+ return;
+ }
+ }
+
+ List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
+ for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
+ sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
+ }
+ List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
+ sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
+ }
+
+ if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
+ result.addError(
+ Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
+ sigAlgsFromSignaturesRecord,
+ sigAlgsFromDigestsRecord);
+ return;
+ }
+
+ // Parse the additional attributes block.
+ int additionalAttributeCount = 0;
+ Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
+ Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1);
+ while (additionalAttributes.hasRemaining()) {
+ additionalAttributeCount++;
+ try {
+ ByteBuffer attribute =
+ ApkSigningBlockUtils.getLengthPrefixedSlice(additionalAttributes);
+ int id = attribute.getInt();
+ byte[] value = ByteBufferUtils.toByteArray(attribute);
+ result.additionalAttributes.add(
+ new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
+ switch (id) {
+ case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID:
+ // stripping protection added when signing with a newer scheme
+ int foundId = ByteBuffer.wrap(value).order(
+ ByteOrder.LITTLE_ENDIAN).getInt();
+ if (supportedApkSigSchemeIds.contains(foundId)) {
+ supportedExpectedApkSigSchemeIds.add(foundId);
+ } else {
+ result.addWarning(
+ Issue.V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID, result.index, foundId);
+ }
+ break;
+ default:
+ result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
+ }
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addError(
+ Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
+ return;
+ }
+ }
+
+ // make sure that all known IDs indicated in stripping protection have already verified
+ for (int id : supportedExpectedApkSigSchemeIds) {
+ if (!foundApkSigSchemeIds.contains(id)) {
+ String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
+ result.addError(
+ Issue.V2_SIG_MISSING_APK_SIG_REFERENCED,
+ result.index,
+ apkSigSchemeName);
+ }
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java
new file mode 100644
index 0000000000..dd92da344a
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v3;
+
+import com.android.apksig.internal.util.AndroidSdkVersion;
+
+/** Constants used by the V3 Signature Scheme signing and verification. */
+public class V3SchemeConstants {
+ private V3SchemeConstants() {}
+
+ public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+ public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61;
+ public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
+
+ public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P;
+ public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T;
+ /**
+ * By default, APK signing key rotation will target T, but packages that have previously
+ * rotated can continue rotating on pre-T by specifying an SDK version <= 32 as the
+ * --rotation-min-sdk-version parameter when using apksigner or when invoking
+ * {@link com.android.apksig.ApkSigner.Builder#setMinSdkVersionForRotation(int)}.
+ */
+ public static final int DEFAULT_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.T;
+
+ /**
+ * This attribute is intended to be written to the V3.0 signer block as an additional attribute
+ * whose value is the minimum SDK version supported for rotation by the V3.1 signing block. If
+ * this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version
+ * for rotation in the v3.1 signing block is not X, then the APK should be rejected.
+ */
+ public static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02;
+
+ /**
+ * This attribute is written to the V3.1 signer block as an additional attribute to signify that
+ * the rotation-min-sdk-version is targeting a development release. This is required to support
+ * testing rotation on new development releases as the previous platform release SDK version
+ * is used as the development release SDK version until the development release SDK is
+ * finalized.
+ */
+ public static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba;
+
+ /**
+ * The current development release; rotation / signing configs targeting this release should
+ * be written with the {@link #PROD_RELEASE} SDK version and the dev release attribute.
+ */
+ public static final int DEV_RELEASE = AndroidSdkVersion.U;
+
+ /**
+ * The current production release.
+ */
+ public static final int PROD_RELEASE = AndroidSdkVersion.T;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
new file mode 100644
index 0000000000..28f6589710
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java
@@ -0,0 +1,531 @@
+/*
+ * 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.SigningSchemeBlockAndDigests;
+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;
+import java.util.OptionalInt;
+
+/**
+ * APK Signature Scheme v3 signer.
+ *
+ * <p>APK Signature Scheme v3 builds upon APK Signature Scheme v3, and maintains all of the APK
+ * Signature Scheme v2 goals.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ * <p>The main contribution of APK Signature Scheme v3 is the introduction of the {@link
+ * SigningCertificateLineage}, which enables an APK to change its signing certificate as long as
+ * it can prove the new siging certificate was signed by the old.
+ */
+public class V3SchemeSigner {
+ public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
+ V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
+
+ private final RunnablesExecutor mExecutor;
+ private final DataSource mBeforeCentralDir;
+ private final DataSource mCentralDir;
+ private final DataSource mEocd;
+ private final List<SignerConfig> mSignerConfigs;
+ private final int mBlockId;
+ private final OptionalInt mOptionalV31MinSdkVersion;
+ private final boolean mRotationTargetsDevRelease;
+
+ private V3SchemeSigner(DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs,
+ RunnablesExecutor executor,
+ int blockId,
+ OptionalInt optionalV31MinSdkVersion,
+ boolean rotationTargetsDevRelease) {
+ mBeforeCentralDir = beforeCentralDir;
+ mCentralDir = centralDir;
+ mEocd = eocd;
+ mSignerConfigs = signerConfigs;
+ mExecutor = executor;
+ mBlockId = blockId;
+ mOptionalV31MinSdkVersion = optionalV31MinSdkVersion;
+ mRotationTargetsDevRelease = rotationTargetsDevRelease;
+ }
+
+ /**
+ * Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the
+ * provided key.
+ *
+ * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
+ * AndroidManifest.xml minSdkVersion attribute).
+ * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
+ * Signature Scheme v3
+ */
+ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+ int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
+ throws InvalidKeyException {
+ String keyAlgorithm = signingKey.getAlgorithm();
+ if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
+ // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
+ // deterministic signatures which make life easier for OTA updates (fewer files
+ // changed when deterministic signature schemes are used).
+
+ // Pick a digest which is no weaker than the key.
+ int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
+ if (modulusLengthBits <= 3072) {
+ // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
+ List<SignatureAlgorithm> algorithms = new ArrayList<>();
+ algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
+ if (verityEnabled) {
+ algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
+ }
+ return algorithms;
+ } else {
+ // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
+ // digest being the weak link. SHA-512 is the next strongest supported digest.
+ return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
+ }
+ } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+ // DSA is supported only with SHA-256.
+ List<SignatureAlgorithm> algorithms = new ArrayList<>();
+ algorithms.add(
+ deterministicDsaSigning ?
+ SignatureAlgorithm.DETDSA_WITH_SHA256 :
+ SignatureAlgorithm.DSA_WITH_SHA256);
+ if (verityEnabled) {
+ algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
+ }
+ return algorithms;
+ } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+ // Pick a digest which is no weaker than the key.
+ int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
+ if (keySizeBits <= 256) {
+ // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
+ List<SignatureAlgorithm> algorithms = new ArrayList<>();
+ algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
+ if (verityEnabled) {
+ algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
+ }
+ return algorithms;
+ } else {
+ // Keys longer than 256 bit need to be paired with a stronger digest to avoid the
+ // digest being the weak link. SHA-512 is the next strongest supported digest.
+ return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
+ }
+ } else {
+ throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+ }
+ }
+
+ public static SigningSchemeBlockAndDigests generateApkSignatureSchemeV3Block(
+ RunnablesExecutor executor,
+ DataSource beforeCentralDir,
+ DataSource centralDir,
+ DataSource eocd,
+ List<SignerConfig> signerConfigs)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+ return new V3SchemeSigner.Builder(beforeCentralDir, centralDir, eocd, signerConfigs)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
+ .build()
+ .generateApkSignatureSchemeV3BlockAndDigests();
+ }
+
+ public static byte[] generateV3SignerAttribute(
+ SigningCertificateLineage signingCertificateLineage) {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: attribute pair
+ // * uint32: ID
+ // * bytes: value - encoded V3 SigningCertificateLineage
+ byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage();
+ int payloadSize = 4 + 4 + encodedLineage.length;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(4 + encodedLineage.length);
+ result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID);
+ result.put(encodedLineage);
+ return result.array();
+ }
+
+ private static byte[] generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+ int rotationMinSdkVersion) {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: attribute pair
+ // * uint32: ID
+ // * bytes: value - int32 representing minimum SDK version for rotation
+ int payloadSize = 4 + 4 + 4;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(payloadSize - 4);
+ result.putInt(V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID);
+ result.putInt(rotationMinSdkVersion);
+ return result.array();
+ }
+
+ private static byte[] generateV31RotationTargetsDevReleaseAttribute() {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: attribute pair
+ // * uint32: ID
+ // * bytes: value - No value is used for this attribute
+ int payloadSize = 4 + 4;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(payloadSize - 4);
+ result.putInt(V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
+ return result.array();
+ }
+
+ /**
+ * Generates and returns a new {@link SigningSchemeBlockAndDigests} containing the V3.x
+ * signing scheme block and digests based on the parameters provided to the {@link Builder}.
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
+ * missing
+ * @throws InvalidKeyException if the X.509 encoded form of the public key cannot be obtained
+ * @throws SignatureException if an error occurs when computing digests or generating
+ * signatures
+ */
+ public SigningSchemeBlockAndDigests generateApkSignatureSchemeV3BlockAndDigests()
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
+ Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
+ ApkSigningBlockUtils.computeContentDigests(
+ mExecutor, mBeforeCentralDir, mCentralDir, mEocd, mSignerConfigs);
+ return new SigningSchemeBlockAndDigests(
+ generateApkSignatureSchemeV3Block(digestInfo.getSecond()), digestInfo.getSecond());
+ }
+
+ private Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
+ Map<ContentDigestAlgorithm, byte[]> contentDigests)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed signer blocks.
+ List<byte[]> signerBlocks = new ArrayList<>(mSignerConfigs.size());
+ int signerNumber = 0;
+ for (SignerConfig signerConfig : mSignerConfigs) {
+ 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),
+ }),
+ mBlockId);
+ }
+
+ private byte[] generateSignerBlock(
+ SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ if (signerConfig.certificates.isEmpty()) {
+ throw new SignatureException("No certificates configured for signer");
+ }
+ PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+
+ byte[] encodedPublicKey = encodePublicKey(publicKey);
+
+ V3SignatureSchemeBlock.SignedData signedData = new V3SignatureSchemeBlock.SignedData();
+ try {
+ signedData.certificates = encodeCertificates(signerConfig.certificates);
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException("Failed to encode certificates", e);
+ }
+
+ List<Pair<Integer, byte[]>> digests =
+ new ArrayList<>(signerConfig.signatureAlgorithms.size());
+ for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
+ ContentDigestAlgorithm contentDigestAlgorithm =
+ signatureAlgorithm.getContentDigestAlgorithm();
+ byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
+ if (contentDigest == null) {
+ throw new RuntimeException(
+ contentDigestAlgorithm
+ + " content digest for "
+ + signatureAlgorithm
+ + " not computed");
+ }
+ digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
+ }
+ signedData.digests = digests;
+ signedData.minSdkVersion = signerConfig.minSdkVersion;
+ signedData.maxSdkVersion = signerConfig.maxSdkVersion;
+ signedData.additionalAttributes = generateAdditionalAttributes(signerConfig);
+
+ V3SignatureSchemeBlock.Signer signer = new V3SignatureSchemeBlock.Signer();
+
+ signer.signedData = encodeSignedData(signedData);
+
+ signer.minSdkVersion = signerConfig.minSdkVersion;
+ signer.maxSdkVersion = signerConfig.maxSdkVersion;
+ signer.publicKey = encodedPublicKey;
+ signer.signatures =
+ ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
+
+ return encodeSigner(signer);
+ }
+
+ private 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 byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
+ byte[] digests =
+ encodeAsLengthPrefixedElement(
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
+ signedData.digests));
+ byte[] certs =
+ encodeAsLengthPrefixedElement(
+ encodeAsSequenceOfLengthPrefixedElements(signedData.certificates));
+ byte[] attributes = encodeAsLengthPrefixedElement(signedData.additionalAttributes);
+
+ // FORMAT:
+ // * length-prefixed sequence of length-prefixed digests:
+ // * uint32: signature algorithm ID
+ // * length-prefixed bytes: digest of contents
+ // * length-prefixed sequence of certificates:
+ // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
+ // * uint-32: minSdkVersion
+ // * uint-32: maxSdkVersion
+ // * length-prefixed sequence of length-prefixed additional attributes:
+ // * uint32: ID
+ // * (length - 4) bytes: value
+ // * uint32: Proof-of-rotation ID: 0x3ba06f8c
+ // * length-prefixed roof-of-rotation structure
+ int payloadSize = digests.length + certs.length + 4 + 4 + attributes.length;
+
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.put(digests);
+ result.put(certs);
+ result.putInt(signedData.minSdkVersion);
+ result.putInt(signedData.maxSdkVersion);
+ result.put(attributes);
+
+ return result.array();
+ }
+
+ private byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
+ List<byte[]> attributes = new ArrayList<>();
+ if (signerConfig.signingCertificateLineage != null) {
+ attributes.add(generateV3SignerAttribute(signerConfig.signingCertificateLineage));
+ }
+ if ((mRotationTargetsDevRelease || signerConfig.signerTargetsDevRelease)
+ && mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
+ attributes.add(generateV31RotationTargetsDevReleaseAttribute());
+ }
+ if (mOptionalV31MinSdkVersion.isPresent()
+ && mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
+ attributes.add(generateV3RotationMinSdkVersionStrippingProtectionAttribute(
+ mOptionalV31MinSdkVersion.getAsInt()));
+ }
+ int attributesSize = attributes.stream().mapToInt(attribute -> attribute.length).sum();
+ byte[] attributesBuffer = new byte[attributesSize];
+ if (attributesSize == 0) {
+ return new byte[0];
+ }
+ int index = 0;
+ for (byte[] attribute : attributes) {
+ System.arraycopy(attribute, 0, attributesBuffer, index, attribute.length);
+ index += attribute.length;
+ }
+ return attributesBuffer;
+ }
+
+ private static final class V3SignatureSchemeBlock {
+ private static final class Signer {
+ public byte[] signedData;
+ public int minSdkVersion;
+ public int maxSdkVersion;
+ public List<Pair<Integer, byte[]>> signatures;
+ public byte[] publicKey;
+ }
+
+ private static final class SignedData {
+ public List<Pair<Integer, byte[]>> digests;
+ public List<byte[]> certificates;
+ public int minSdkVersion;
+ public int maxSdkVersion;
+ public byte[] additionalAttributes;
+ }
+ }
+
+ /** Builder of {@link V3SchemeSigner} instances. */
+ public static class Builder {
+ private final DataSource mBeforeCentralDir;
+ private final DataSource mCentralDir;
+ private final DataSource mEocd;
+ private final List<SignerConfig> mSignerConfigs;
+
+ private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
+ private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ private OptionalInt mOptionalV31MinSdkVersion = OptionalInt.empty();
+ private boolean mRotationTargetsDevRelease = false;
+
+ /**
+ * Instantiates a new {@code Builder} with an APK's {@code beforeCentralDir}, {@code
+ * centralDir}, and {@code eocd}, along with a {@link List} of {@code signerConfigs} to
+ * be used to sign the APK.
+ */
+ public Builder(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd,
+ List<SignerConfig> signerConfigs) {
+ mBeforeCentralDir = beforeCentralDir;
+ mCentralDir = centralDir;
+ mEocd = eocd;
+ mSignerConfigs = signerConfigs;
+ }
+
+ /**
+ * Sets the {@link RunnablesExecutor} to be used when computing the APK's content digests.
+ */
+ public Builder setRunnablesExecutor(RunnablesExecutor executor) {
+ mExecutor = executor;
+ return this;
+ }
+
+ /**
+ * Sets the {@code blockId} to be used for the V3 signature block.
+ *
+ * <p>This {@code V3SchemeSigner} currently supports the block IDs for the {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
+ */
+ public Builder setBlockId(int blockId) {
+ mBlockId = blockId;
+ return this;
+ }
+
+ /**
+ * Sets the {@code rotationMinSdkVersion} to be written as an additional attribute in each
+ * signer's block.
+ *
+ * <p>This value provides stripping protection to ensure a v3.1 signing block with rotation
+ * is not modified or removed from the APK's signature block.
+ */
+ public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
+ return setMinSdkVersionForV31(rotationMinSdkVersion);
+ }
+
+ /**
+ * Sets the {@code minSdkVersion} to be written as an additional attribute in each
+ * signer's block.
+ *
+ * <p>This value provides the stripping protection to ensure a v3.1 signing block is not
+ * modified or removed from the APK's signature block.
+ */
+ public Builder setMinSdkVersionForV31(int minSdkVersion) {
+ if (minSdkVersion == V3SchemeConstants.DEV_RELEASE) {
+ minSdkVersion = V3SchemeConstants.PROD_RELEASE;
+ }
+ mOptionalV31MinSdkVersion = OptionalInt.of(minSdkVersion);
+ return this;
+ }
+
+ /**
+ * Sets whether the minimum SDK version of a signer is intended to target a development
+ * release; this is primarily required after the T SDK is finalized, and an APK needs to
+ * target U during its development cycle for rotation.
+ *
+ * <p>This is only required after the T SDK is finalized since S and earlier releases do
+ * not know about the V3.1 block ID, but once T is released and work begins on U, U will
+ * use the SDK version of T during development. A signer with a minimum SDK version of T's
+ * SDK version along with setting {@code enabled} to true will allow an APK to use the
+ * rotated key on a device running U while causing this to be bypassed for T.
+ *
+ * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
+ * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
+ * will be a noop.
+ */
+ public Builder setRotationTargetsDevRelease(boolean enabled) {
+ mRotationTargetsDevRelease = enabled;
+ return this;
+ }
+
+ /**
+ * Returns a new {@link V3SchemeSigner} built with the configuration provided to this
+ * {@code Builder}.
+ */
+ public V3SchemeSigner build() {
+ return new V3SchemeSigner(mBeforeCentralDir,
+ mCentralDir,
+ mEocd,
+ mSignerConfigs,
+ mExecutor,
+ mBlockId,
+ mOptionalV31MinSdkVersion,
+ mRotationTargetsDevRelease);
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
new file mode 100644
index 0000000000..bd808f0e66
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
@@ -0,0 +1,783 @@
+/*
+ * 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.ApkVerificationIssue;
+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.ByteBufferUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.util.RunnablesExecutor;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.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.OptionalInt;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * APK Signature Scheme v3 verifier.
+ *
+ * <p>APK Signature Scheme v3, like v2 is a whole-file signature scheme which aims to protect every
+ * single bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
+ * uncompressed contents of ZIP entries.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ */
+public class V3SchemeVerifier {
+ private final RunnablesExecutor mExecutor;
+ private final DataSource mApk;
+ private final ApkUtils.ZipSections mZipSections;
+ private final ApkSigningBlockUtils.Result mResult;
+ private final Set<ContentDigestAlgorithm> mContentDigestsToVerify;
+ private final int mMinSdkVersion;
+ private final int mMaxSdkVersion;
+ private final int mBlockId;
+ private final OptionalInt mOptionalRotationMinSdkVersion;
+ private final boolean mFullVerification;
+
+ private ByteBuffer mApkSignatureSchemeV3Block;
+
+ private V3SchemeVerifier(
+ RunnablesExecutor executor,
+ DataSource apk,
+ ApkUtils.ZipSections zipSections,
+ Set<ContentDigestAlgorithm> contentDigestsToVerify,
+ ApkSigningBlockUtils.Result result,
+ int minSdkVersion,
+ int maxSdkVersion,
+ int blockId,
+ OptionalInt optionalRotationMinSdkVersion,
+ boolean fullVerification) {
+ mExecutor = executor;
+ mApk = apk;
+ mZipSections = zipSections;
+ mContentDigestsToVerify = contentDigestsToVerify;
+ mResult = result;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ mBlockId = blockId;
+ mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
+ mFullVerification = fullVerification;
+ }
+
+ /**
+ * Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of
+ * verification. The APK must be considered verified only if
+ * {@link ApkSigningBlockUtils.Result#verified} is
+ * {@code true}. If verification fails, the result will contain errors -- see
+ * {@link ApkSigningBlockUtils.Result#getErrors()}.
+ *
+ * <p>Verification succeeds iff the APK's APK Signature Scheme v3 signatures are expected to
+ * verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
+ * If the APK's signature is expected to not verify on any of the specified platform versions,
+ * this method returns a result with one or more errors and whose
+ * {@code Result.verified == false}, or this method throws an exception.
+ *
+ * <p>This method only verifies the v3.0 signing block without platform targeted rotation from
+ * a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence
+ * of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}.
+ *
+ * @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 {
+ return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion)
+ .setRunnablesExecutor(executor)
+ .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
+ .build()
+ .verify();
+ }
+
+ /**
+ * 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.
+ *
+ * @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the
+ * APK, such as information about signers, and verification errors and warnings
+ */
+ public ApkSigningBlockUtils.Result verify()
+ throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+ if (mApk == null || mZipSections == null) {
+ throw new IllegalStateException(
+ "A non-null apk and zip sections must be specified to verify an APK's v3 "
+ + "signatures");
+ }
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
+ mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+
+ DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset);
+ DataSource centralDir =
+ mApk.slice(
+ signatureInfo.centralDirOffset,
+ signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
+ ByteBuffer eocd = signatureInfo.eocd;
+
+ parseSigners();
+
+ if (mResult.containsErrors()) {
+ return mResult;
+ }
+ ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd,
+ mContentDigestsToVerify, mResult);
+
+ // make sure that the v3 signers cover the entire targeted sdk version ranges and that the
+ // longest SigningCertificateHistory, if present, corresponds to the newest platform
+ // versions
+ SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
+ for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) {
+ sortedSigners.put(signer.maxSdkVersion, signer);
+ }
+
+ // first make sure there is neither overlap nor holes
+ int firstMin = 0;
+ int lastMax = 0;
+ int lastLineageSize = 0;
+
+ // while we're iterating through the signers, build up the list of lineages
+ List<SigningCertificateLineage> lineages = new ArrayList<>(mResult.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 {
+ // A signer's minimum SDK can equal the previous signer's maximum SDK if this signer
+ // is targeting a development release.
+ if (currentMin != (lastMax + 1)
+ && !(currentMin == lastMax && signerTargetsDevRelease(signer))) {
+ mResult.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) {
+ mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
+ break;
+ }
+ lastLineageSize = currLineageSize;
+ lineages.add(signer.signingCertificateLineage);
+ }
+ }
+
+ // make sure we support our desired sdk ranges; if rotation is present in a v3.1 block
+ // then the max level only needs to support up to that sdk version for rotation.
+ if (firstMin > mMinSdkVersion
+ || lastMax < (mOptionalRotationMinSdkVersion.isPresent()
+ ? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) {
+ mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
+ }
+
+ try {
+ mResult.signingCertificateLineage =
+ SigningCertificateLineage.consolidateLineages(lineages);
+ } catch (IllegalArgumentException e) {
+ mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
+ }
+ if (!mResult.containsErrors()) {
+ mResult.verified = true;
+ }
+ return mResult;
+ }
+
+ /**
+ * Parses each signer in the provided APK Signature Scheme v3 block and populates corresponding
+ * {@code signerInfos} of the provided {@code result}.
+ *
+ * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
+ * However, this does not verify the integrity of the rest of the APK but rather simply reports
+ * the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the
+ * {@code [minSdkVersion, maxSdkVersion]} range.
+ */
+ public static void parseSigners(
+ ByteBuffer apkSignatureSchemeV3Block,
+ Set<ContentDigestAlgorithm> contentDigestsToVerify,
+ ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
+ try {
+ new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block)
+ .setResult(result)
+ .setContentDigestsToVerify(contentDigestsToVerify)
+ .setFullVerification(false)
+ .build()
+ .parseSigners();
+ } catch (IOException | SignatureNotFoundException e) {
+ // This should never occur since the apkSignatureSchemeV3Block was already provided.
+ throw new IllegalStateException("An exception was encountered when attempting to parse"
+ + " the signers from the provided APK Signature Scheme v3 block", e);
+ }
+ }
+
+ /**
+ * Parses each signer in the APK Signature Scheme v3 block and populates corresponding
+ * {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the
+ * returned {@link ApkSigningBlockUtils.Result}.
+ *
+ * <p>This verifies signatures over {@code signed-data} block contained in each signer block.
+ * However, this does not verify the integrity of the rest of the APK but rather simply reports
+ * the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}).
+ *
+ * <p>This method adds one or more errors to the returned {@code Result} if a verification error
+ * is encountered when parsing the signers.
+ */
+ public ApkSigningBlockUtils.Result parseSigners()
+ throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
+ ByteBuffer signers;
+ try {
+ if (mApkSignatureSchemeV3Block == null) {
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
+ mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+ }
+ signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block);
+ } catch (ApkFormatException e) {
+ mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
+ return mResult;
+ }
+ if (!signers.hasRemaining()) {
+ mResult.addError(Issue.V3_SIG_NO_SIGNERS);
+ return mResult;
+ }
+
+ 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;
+ mResult.signers.add(signerInfo);
+ try {
+ ByteBuffer signer = getLengthPrefixedSlice(signers);
+ parseSigner(signer, certFactory, signerInfo);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER);
+ return mResult;
+ }
+ }
+ return mResult;
+ }
+
+ /**
+ * Parses the provided signer block and populates the {@code result}.
+ *
+ * <p>This verifies signatures over {@code signed-data} contained in this block, as well as
+ * the data contained therein, but does not verify the integrity of the rest of the APK. To
+ * facilitate APK integrity verification, this method adds the {@code contentDigestsToVerify}.
+ * These digests can then be used to verify the integrity of the APK.
+ *
+ * <p>This method adds one or more errors to the {@code result} if a verification error is
+ * expected to be encountered on an Android platform version in the
+ * {@code [minSdkVersion, maxSdkVersion]} range.
+ */
+ private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory,
+ ApkSigningBlockUtils.Result.SignerInfo result)
+ throws ApkFormatException, NoSuchAlgorithmException {
+ ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
+ byte[] signedDataBytes = new byte[signedData.remaining()];
+ signedData.get(signedDataBytes);
+ signedData.flip();
+ result.signedData = signedDataBytes;
+
+ int parsedMinSdkVersion = signerBlock.getInt();
+ int parsedMaxSdkVersion = signerBlock.getInt();
+ result.minSdkVersion = parsedMinSdkVersion;
+ result.maxSdkVersion = parsedMaxSdkVersion;
+ if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) {
+ result.addError(
+ Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion);
+ }
+ ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
+ byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
+
+ // Parse the signatures block and identify supported signatures
+ int signatureCount = 0;
+ List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
+ while (signatures.hasRemaining()) {
+ signatureCount++;
+ try {
+ ByteBuffer signature = getLengthPrefixedSlice(signatures);
+ int sigAlgorithmId = signature.getInt();
+ byte[] sigBytes = readLengthPrefixedByteArray(signature);
+ result.signatures.add(
+ new ApkSigningBlockUtils.Result.SignerInfo.Signature(
+ sigAlgorithmId, sigBytes));
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+ if (signatureAlgorithm == null) {
+ result.addWarning(Issue.V3_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+ continue;
+ }
+ // TODO consider dropping deprecated signatures for v3 or modifying
+ // getSignaturesToVerify (called below)
+ supportedSignatures.add(
+ new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addError(Issue.V3_SIG_MALFORMED_SIGNATURE, signatureCount);
+ return;
+ }
+ }
+ if (result.signatures.isEmpty()) {
+ result.addError(Issue.V3_SIG_NO_SIGNATURES);
+ return;
+ }
+
+ // Verify signatures over signed-data block using the public key
+ List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
+ try {
+ signaturesToVerify =
+ ApkSigningBlockUtils.getSignaturesToVerify(
+ supportedSignatures, result.minSdkVersion, result.maxSdkVersion);
+ } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
+ result.addError(Issue.V3_SIG_NO_SUPPORTED_SIGNATURES);
+ return;
+ }
+ for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
+ SignatureAlgorithm signatureAlgorithm = signature.algorithm;
+ String jcaSignatureAlgorithm =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+ String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
+ PublicKey publicKey;
+ try {
+ publicKey =
+ KeyFactory.getInstance(keyAlgorithm).generatePublic(
+ new X509EncodedKeySpec(publicKeyBytes));
+ } catch (Exception e) {
+ result.addError(Issue.V3_SIG_MALFORMED_PUBLIC_KEY, e);
+ return;
+ }
+ try {
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ signedData.position(0);
+ sig.update(signedData);
+ byte[] sigBytes = signature.signature;
+ if (!sig.verify(sigBytes)) {
+ result.addError(Issue.V3_SIG_DID_NOT_VERIFY, signatureAlgorithm);
+ return;
+ }
+ result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+ mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException
+ | SignatureException e) {
+ result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
+ return;
+ }
+ }
+
+ // At least one signature over signedData has verified. We can now parse signed-data.
+ signedData.position(0);
+ ByteBuffer digests = getLengthPrefixedSlice(signedData);
+ ByteBuffer certificates = getLengthPrefixedSlice(signedData);
+
+ int signedMinSdkVersion = signedData.getInt();
+ if (signedMinSdkVersion != parsedMinSdkVersion) {
+ result.addError(
+ Issue.V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
+ parsedMinSdkVersion,
+ signedMinSdkVersion);
+ }
+ int signedMaxSdkVersion = signedData.getInt();
+ if (signedMaxSdkVersion != parsedMaxSdkVersion) {
+ result.addError(
+ Issue.V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
+ parsedMaxSdkVersion,
+ signedMaxSdkVersion);
+ }
+ ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
+
+ // Parse the certificates block
+ int certificateIndex = -1;
+ while (certificates.hasRemaining()) {
+ certificateIndex++;
+ byte[] encodedCert = readLengthPrefixedByteArray(certificates);
+ X509Certificate certificate;
+ try {
+ certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
+ } catch (CertificateException e) {
+ result.addError(
+ Issue.V3_SIG_MALFORMED_CERTIFICATE,
+ certificateIndex,
+ certificateIndex + 1,
+ e);
+ return;
+ }
+ // Wrap the cert so that the result's getEncoded returns exactly the original encoded
+ // form. Without this, getEncoded may return a different form from what was stored in
+ // the signature. This is because some X509Certificate(Factory) implementations
+ // re-encode certificates.
+ certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
+ result.certs.add(certificate);
+ }
+
+ if (result.certs.isEmpty()) {
+ result.addError(Issue.V3_SIG_NO_CERTIFICATES);
+ return;
+ }
+ X509Certificate mainCertificate = result.certs.get(0);
+ byte[] certificatePublicKeyBytes;
+ try {
+ certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
+ mainCertificate.getPublicKey());
+ } catch (InvalidKeyException e) {
+ System.out.println("Caught an exception encoding the public key: " + e);
+ e.printStackTrace();
+ certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
+ }
+ if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+ result.addError(
+ Issue.V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+ ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
+ ApkSigningBlockUtils.toHex(publicKeyBytes));
+ return;
+ }
+
+ // Parse the digests block
+ int digestCount = 0;
+ while (digests.hasRemaining()) {
+ digestCount++;
+ try {
+ ByteBuffer digest = getLengthPrefixedSlice(digests);
+ int sigAlgorithmId = digest.getInt();
+ byte[] digestBytes = readLengthPrefixedByteArray(digest);
+ result.contentDigests.add(
+ new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
+ sigAlgorithmId, digestBytes));
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addError(Issue.V3_SIG_MALFORMED_DIGEST, digestCount);
+ return;
+ }
+ }
+
+ List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
+ for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
+ sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
+ }
+ List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
+ sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
+ }
+
+ if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
+ result.addError(
+ Issue.V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
+ sigAlgsFromSignaturesRecord,
+ sigAlgsFromDigestsRecord);
+ return;
+ }
+
+ // Parse the additional attributes block.
+ int additionalAttributeCount = 0;
+ boolean rotationAttrFound = false;
+ while (additionalAttributes.hasRemaining()) {
+ additionalAttributeCount++;
+ try {
+ ByteBuffer attribute =
+ getLengthPrefixedSlice(additionalAttributes);
+ int id = attribute.getInt();
+ byte[] value = ByteBufferUtils.toByteArray(attribute);
+ result.additionalAttributes.add(
+ new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
+ if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
+ try {
+ // SigningCertificateLineage is verified when built
+ result.signingCertificateLineage =
+ SigningCertificateLineage.readFromV3AttributeValue(value);
+ // make sure that the last cert in the chain matches this signer cert
+ SigningCertificateLineage subLineage =
+ result.signingCertificateLineage.getSubLineage(result.certs.get(0));
+ if (result.signingCertificateLineage.size() != subLineage.size()) {
+ result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
+ }
+ } catch (SecurityException e) {
+ result.addError(Issue.V3_SIG_POR_DID_NOT_VERIFY);
+ } catch (IllegalArgumentException e) {
+ result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
+ } catch (Exception e) {
+ result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
+ }
+ } else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) {
+ rotationAttrFound = true;
+ // API targeting for rotation was added with V3.1; if the maxSdkVersion
+ // does not support v3.1 then ignore this attribute.
+ if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT
+ && mFullVerification) {
+ int attrRotationMinSdkVersion = ByteBuffer.wrap(value)
+ .order(ByteOrder.LITTLE_ENDIAN).getInt();
+ if (mOptionalRotationMinSdkVersion.isPresent()) {
+ int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt();
+ if (attrRotationMinSdkVersion != rotationMinSdkVersion) {
+ result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH,
+ attrRotationMinSdkVersion, rotationMinSdkVersion);
+ }
+ } else {
+ result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion);
+ }
+ }
+ } else if (id == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) {
+ // This attribute should only be used by a v3.1 signer to indicate rotation
+ // is targeting the development release that is using the SDK version of the
+ // previously released platform version.
+ if (mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
+ result.addWarning(Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER);
+ }
+ } else {
+ result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
+ }
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addError(
+ Issue.V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
+ return;
+ }
+ }
+ if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) {
+ result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING,
+ mOptionalRotationMinSdkVersion.getAsInt());
+ }
+ }
+
+ /**
+ * Returns whether the specified {@code signerInfo} is targeting a development release.
+ */
+ public static boolean signerTargetsDevRelease(
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo) {
+ boolean result = signerInfo.additionalAttributes.stream()
+ .mapToInt(attribute -> attribute.getId())
+ .anyMatch(attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
+ return result;
+ }
+
+ /** Builder of {@link V3SchemeVerifier} instances. */
+ public static class Builder {
+ private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
+ private DataSource mApk;
+ private ApkUtils.ZipSections mZipSections;
+ private ByteBuffer mApkSignatureSchemeV3Block;
+ private Set<ContentDigestAlgorithm> mContentDigestsToVerify;
+ private ApkSigningBlockUtils.Result mResult;
+ private int mMinSdkVersion;
+ private int mMaxSdkVersion;
+ private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ private boolean mFullVerification = true;
+ private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
+
+ /**
+ * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
+ * verify the V3 signing block of the provided {@code apk} with the specified {@code
+ * zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}.
+ */
+ public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion,
+ int maxSdkVersion) {
+ mApk = apk;
+ mZipSections = zipSections;
+ mMinSdkVersion = minSdkVersion;
+ mMaxSdkVersion = maxSdkVersion;
+ }
+
+ /**
+ * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
+ * parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code
+ * apkSignatureSchemeV3Block}.
+ *
+ * <note>Full verification of the v3 signature is not possible when instantiating a new
+ * {@code V3SchemeVerifier} with this method.</note>
+ */
+ public Builder(ByteBuffer apkSignatureSchemeV3Block) {
+ mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block;
+ }
+
+ /**
+ * Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests.
+ */
+ public Builder setRunnablesExecutor(RunnablesExecutor executor) {
+ mExecutor = executor;
+ return this;
+ }
+
+ /**
+ * Sets the V3 {code blockId} to be verified in the provided APK.
+ *
+ * <p>This {@code V3SchemeVerifier} currently supports the block IDs for the {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
+ * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
+ */
+ public Builder setBlockId(int blockId) {
+ mBlockId = blockId;
+ return this;
+ }
+
+ /**
+ * Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional
+ * attribute.
+ *
+ * <p>This value can be obtained from the signers returned when verifying the v3.1 signing
+ * block of an APK; in the case of multiple signers targeting different SDK versions in the
+ * v3.1 signing block, the minimum SDK version from all the signers should be used.
+ */
+ public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
+ mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
+ return this;
+ }
+
+ /**
+ * Sets the {@code result} instance to be used when returning verification results.
+ *
+ * <p>This method can be used when the caller already has a {@link
+ * ApkSigningBlockUtils.Result} and wants to store the verification results in this
+ * instance.
+ */
+ public Builder setResult(ApkSigningBlockUtils.Result result) {
+ mResult = result;
+ return this;
+ }
+
+ /**
+ * Sets the instance to be used to store the {@code contentDigestsToVerify}.
+ *
+ * <p>This method can be used when the caller needs access to the {@code
+ * contentDigestsToVerify} computed by this {@code V3SchemeVerifier}.
+ */
+ public Builder setContentDigestsToVerify(
+ Set<ContentDigestAlgorithm> contentDigestsToVerify) {
+ mContentDigestsToVerify = contentDigestsToVerify;
+ return this;
+ }
+
+ /**
+ * Sets whether full verification should be performed by the {@code V3SchemeVerifier} built
+ * from this instance.
+ *
+ * <note>{@link #verify()} will always verify the content digests for the APK, but this
+ * allows verification of the rotation minimum SDK version stripping attribute to be skipped
+ * for scenarios where this value may not have been parsed from a V3.1 signing block (such
+ * as when only {@link #parseSigners()} will be invoked.</note>
+ */
+ public Builder setFullVerification(boolean fullVerification) {
+ mFullVerification = fullVerification;
+ return this;
+ }
+
+ /**
+ * Returns a new {@link V3SchemeVerifier} built with the configuration provided to this
+ * {@code Builder}.
+ */
+ public V3SchemeVerifier build() {
+ int sigSchemeVersion;
+ switch (mBlockId) {
+ case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID:
+ sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+ mMinSdkVersion = Math.max(mMinSdkVersion,
+ V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT);
+ break;
+ case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID:
+ sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+ // V3.1 supports targeting an SDK version later than that of the initial release
+ // in which it is supported; allow any range for V3.1 as long as V3.0 covers the
+ // rest of the range.
+ mMinSdkVersion = mMaxSdkVersion;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x",
+ mBlockId));
+ }
+ if (mResult == null) {
+ mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion);
+ }
+ if (mContentDigestsToVerify == null) {
+ mContentDigestsToVerify = new HashSet<>(1);
+ }
+
+ V3SchemeVerifier verifier = new V3SchemeVerifier(
+ mExecutor,
+ mApk,
+ mZipSections,
+ mContentDigestsToVerify,
+ mResult,
+ mMinSdkVersion,
+ mMaxSdkVersion,
+ mBlockId,
+ mOptionalRotationMinSdkVersion,
+ mFullVerification);
+ if (mApkSignatureSchemeV3Block != null) {
+ verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block;
+ }
+ return verifier;
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
new file mode 100644
index 0000000000..4ae7a5365d
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v3;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
+
+import com.android.apksig.apk.ApkFormatException;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * APK Signer Lineage.
+ *
+ * <p>The signer lineage contains a history of signing certificates with each ancestor attesting to
+ * the validity of its descendant. Each additional descendant represents a new identity that can be
+ * used to sign an APK, and each generation has accompanying attributes which represent how the
+ * APK would like to view the older signing certificates, specifically how they should be trusted in
+ * certain situations.
+ *
+ * <p> Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies
+ * the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
+ * Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
+ * allow upgrades to the new certificate.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
+ */
+public class V3SigningCertificateLineage {
+
+ private final static int FIRST_VERSION = 1;
+ private final static int CURRENT_VERSION = FIRST_VERSION;
+
+ /**
+ * Deserializes the binary representation of an {@link V3SigningCertificateLineage}. Also
+ * verifies that the structure is well-formed, e.g. that the signature for each node is from its
+ * parent.
+ */
+ public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
+ throws IOException {
+ List<SigningCertificateNode> result = new ArrayList<>();
+ int nodeCount = 0;
+ if (inputBytes == null || !inputBytes.hasRemaining()) {
+ return null;
+ }
+
+ ApkSigningBlockUtils.checkByteOrderLittleEndian(inputBytes);
+
+ // FORMAT (little endian):
+ // * uint32: version code
+ // * sequence of length-prefixed (uint32): nodes
+ // * length-prefixed bytes: signed data
+ // * length-prefixed bytes: certificate
+ // * uint32: signature algorithm id
+ // * uint32: flags
+ // * uint32: signature algorithm id (used by to sign next cert in lineage)
+ // * length-prefixed bytes: signature over above signed data
+
+ X509Certificate lastCert = null;
+ int lastSigAlgorithmId = 0;
+
+ try {
+ int version = inputBytes.getInt();
+ if (version != CURRENT_VERSION) {
+ // we only have one version to worry about right now, so just check it
+ throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
+ + " different than any of which we are aware");
+ }
+ HashSet<X509Certificate> certHistorySet = new HashSet<>();
+ while (inputBytes.hasRemaining()) {
+ nodeCount++;
+ ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
+ ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
+ int flags = nodeBytes.getInt();
+ int sigAlgorithmId = nodeBytes.getInt();
+ SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
+ byte[] signature = readLengthPrefixedByteArray(nodeBytes);
+
+ if (lastCert != null) {
+ // Use previous level cert to verify current level
+ String jcaSignatureAlgorithm =
+ sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+ sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+ PublicKey publicKey = lastCert.getPublicKey();
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(signedData);
+ if (!sig.verify(signature)) {
+ throw new SecurityException("Unable to verify signature of certificate #"
+ + nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
+ + " V3SigningCertificateLineage object");
+ }
+ }
+
+ signedData.rewind();
+ byte[] encodedCert = readLengthPrefixedByteArray(signedData);
+ int signedSigAlgorithm = signedData.getInt();
+ if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
+ throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ + nodeBytes + " when verifying V3SigningCertificateLineage object");
+ }
+ lastCert = X509CertificateUtils.generateCertificate(encodedCert);
+ lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
+ if (certHistorySet.contains(lastCert)) {
+ throw new SecurityException("Encountered duplicate entries in "
+ + "SigningCertificateLineage at certificate #" + nodeCount + ". All "
+ + "signing certificates should be unique");
+ }
+ certHistorySet.add(lastCert);
+ lastSigAlgorithmId = sigAlgorithmId;
+ result.add(new SigningCertificateNode(
+ lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
+ SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
+ }
+ } catch(ApkFormatException | BufferUnderflowException e){
+ throw new IOException("Failed to parse V3SigningCertificateLineage object", e);
+ } catch(NoSuchAlgorithmException | InvalidKeyException
+ | InvalidAlgorithmParameterException | SignatureException e){
+ throw new SecurityException(
+ "Failed to verify signature over signed data for certificate #" + nodeCount
+ + " when parsing V3SigningCertificateLineage object", e);
+ } catch(CertificateException e){
+ throw new SecurityException("Failed to decode certificate #" + nodeCount
+ + " when parsing V3SigningCertificateLineage object", e);
+ }
+ return result;
+ }
+
+ /**
+ * encode the in-memory representation of this {@code V3SigningCertificateLineage}
+ */
+ public static byte[] encodeSigningCertificateLineage(
+ List<SigningCertificateNode> signingCertificateLineage) {
+ // FORMAT (little endian):
+ // * version code
+ // * sequence of length-prefixed (uint32): nodes
+ // * length-prefixed bytes: signed data
+ // * length-prefixed bytes: certificate
+ // * uint32: signature algorithm id
+ // * uint32: flags
+ // * uint32: signature algorithm id (used by to sign next cert in lineage)
+
+ List<byte[]> nodes = new ArrayList<>();
+ for (SigningCertificateNode node : signingCertificateLineage) {
+ nodes.add(encodeSigningCertificateNode(node));
+ }
+ byte [] encodedSigningCertificateLineage = encodeAsSequenceOfLengthPrefixedElements(nodes);
+
+ // add the version code (uint32) on top of the encoded nodes
+ int payloadSize = 4 + encodedSigningCertificateLineage.length;
+ ByteBuffer encodedWithVersion = ByteBuffer.allocate(payloadSize);
+ encodedWithVersion.order(ByteOrder.LITTLE_ENDIAN);
+ encodedWithVersion.putInt(CURRENT_VERSION);
+ encodedWithVersion.put(encodedSigningCertificateLineage);
+ return encodedWithVersion.array();
+ }
+
+ public static byte[] encodeSigningCertificateNode(SigningCertificateNode node) {
+ // FORMAT (little endian):
+ // * length-prefixed bytes: signed data
+ // * length-prefixed bytes: certificate
+ // * uint32: signature algorithm id
+ // * uint32: flags
+ // * uint32: signature algorithm id (used by to sign next cert in lineage)
+ // * length-prefixed bytes: signature over signed data
+ int parentSigAlgorithmId = 0;
+ if (node.parentSigAlgorithm != null) {
+ parentSigAlgorithmId = node.parentSigAlgorithm.getId();
+ }
+ int sigAlgorithmId = 0;
+ if (node.sigAlgorithm != null) {
+ sigAlgorithmId = node.sigAlgorithm.getId();
+ }
+ byte[] prefixedSignedData = encodeSignedData(node.signingCert, parentSigAlgorithmId);
+ byte[] prefixedSignature = encodeAsLengthPrefixedElement(node.signature);
+ int payloadSize = prefixedSignedData.length + 4 + 4 + prefixedSignature.length;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.put(prefixedSignedData);
+ result.putInt(node.flags);
+ result.putInt(sigAlgorithmId);
+ result.put(prefixedSignature);
+ return result.array();
+ }
+
+ public static byte[] encodeSignedData(X509Certificate certificate, int flags) {
+ try {
+ byte[] prefixedCertificate = encodeAsLengthPrefixedElement(certificate.getEncoded());
+ int payloadSize = 4 + prefixedCertificate.length;
+ ByteBuffer result = ByteBuffer.allocate(payloadSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.put(prefixedCertificate);
+ result.putInt(flags);
+ return encodeAsLengthPrefixedElement(result.array());
+ } catch (CertificateEncodingException e) {
+ throw new RuntimeException(
+ "Failed to encode V3SigningCertificateLineage certificate", e);
+ }
+ }
+
+ /**
+ * Represents one signing certificate in the {@link V3SigningCertificateLineage}, which
+ * generally means it is/was used at some point to sign the same APK of the others in the
+ * lineage.
+ */
+ public static class SigningCertificateNode {
+
+ public SigningCertificateNode(
+ X509Certificate signingCert,
+ SignatureAlgorithm parentSigAlgorithm,
+ SignatureAlgorithm sigAlgorithm,
+ byte[] signature,
+ int flags) {
+ this.signingCert = signingCert;
+ this.parentSigAlgorithm = parentSigAlgorithm;
+ this.sigAlgorithm = sigAlgorithm;
+ this.signature = signature;
+ this.flags = flags;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SigningCertificateNode)) return false;
+
+ SigningCertificateNode that = (SigningCertificateNode) o;
+ if (!signingCert.equals(that.signingCert)) return false;
+ if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
+ if (sigAlgorithm != that.sigAlgorithm) return false;
+ if (!Arrays.equals(signature, that.signature)) return false;
+ if (flags != that.flags) return false;
+
+ // we made it
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(signingCert, parentSigAlgorithm, sigAlgorithm, flags);
+ result = 31 * result + Arrays.hashCode(signature);
+ return result;
+ }
+
+ /**
+ * the signing cert for this node. This is part of the data signed by the parent node.
+ */
+ public final X509Certificate signingCert;
+
+ /**
+ * the algorithm used by the this node's parent to bless this data. Its ID value is part of
+ * the data signed by the parent node. {@code null} for first node.
+ */
+ public final SignatureAlgorithm parentSigAlgorithm;
+
+ /**
+ * the algorithm used by the this nodeto bless the next node's data. Its ID value is part
+ * of the signed data of the next node. {@code null} for the last node.
+ */
+ public SignatureAlgorithm sigAlgorithm;
+
+ /**
+ * signature over the signed data (above). The signature is from this node's parent
+ * signing certificate, which should correspond to the signing certificate used to sign an
+ * APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
+ */
+ public final byte[] signature;
+
+ /**
+ * the flags detailing how the platform should treat this signing cert
+ */
+ public int flags;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
new file mode 100644
index 0000000000..7bf952d82a
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v4;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
+import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+
+import com.android.apksig.apk.ApkUtils;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksig.internal.apk.v3.V3SchemeSigner;
+import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * APK Signature Scheme V4 signer. V4 scheme file contains 2 mandatory fields - used during
+ * installation. And optional verity tree - has to be present during session commit.
+ * <p>
+ * The fields:
+ * <p>
+ * 1. hashingInfo - verity root hash and hashing info,
+ * 2. signingInfo - certificate, public key and signature,
+ * For more details see V4Signature.
+ * </p>
+ * (optional) verityTree: integer size prepended bytes of the verity hash tree.
+ * <p>
+ */
+public abstract class V4SchemeSigner {
+ /**
+ * Hidden constructor to prevent instantiation.
+ */
+ private V4SchemeSigner() {
+ }
+
+ public static class SignerConfig {
+ final public ApkSigningBlockUtils.SignerConfig v4Config;
+ final public ApkSigningBlockUtils.SignerConfig v41Config;
+
+ public SignerConfig(List<ApkSigningBlockUtils.SignerConfig> v4Configs,
+ List<ApkSigningBlockUtils.SignerConfig> v41Configs) throws InvalidKeyException {
+ if (v4Configs == null || v4Configs.size() != 1) {
+ throw new InvalidKeyException("Only accepting one signer config for V4 Signature.");
+ }
+ if (v41Configs != null && v41Configs.size() != 1) {
+ throw new InvalidKeyException("Only accepting one signer config for V4.1 Signature.");
+ }
+ this.v4Config = v4Configs.get(0);
+ this.v41Config = v41Configs != null ? v41Configs.get(0) : null;
+ }
+ }
+
+ /**
+ * Based on a public key, return a signing algorithm that supports verity.
+ */
+ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
+ int minSdkVersion, boolean apkSigningBlockPaddingSupported,
+ boolean deterministicDsaSigning)
+ throws InvalidKeyException {
+ List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
+ signingKey, minSdkVersion,
+ apkSigningBlockPaddingSupported, deterministicDsaSigning);
+ // Keeping only supported algorithms.
+ for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
+ final SignatureAlgorithm algorithm = iter.next();
+ if (!isSupported(algorithm.getContentDigestAlgorithm(), false)) {
+ iter.remove();
+ }
+ }
+ return algorithms;
+ }
+
+ /**
+ * Compute hash tree and generate v4 signature for a given APK. Write the serialized data to
+ * output file.
+ */
+ public static void generateV4Signature(
+ DataSource apkContent, SignerConfig signerConfig, File outputFile)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException {
+ Pair<V4Signature, byte[]> pair = generateV4Signature(apkContent, signerConfig);
+ try (final OutputStream output = new FileOutputStream(outputFile)) {
+ pair.getFirst().writeTo(output);
+ V4Signature.writeBytes(output, pair.getSecond());
+ } catch (IOException e) {
+ outputFile.delete();
+ throw e;
+ }
+ }
+
+ /** Generate v4 signature and hash tree for a given APK. */
+ public static Pair<V4Signature, byte[]> generateV4Signature(
+ DataSource apkContent,
+ SignerConfig signerConfig)
+ throws IOException, InvalidKeyException, NoSuchAlgorithmException {
+ // Salt has to stay empty for fs-verity compatibility.
+ final byte[] salt = null;
+ // Not used by apksigner.
+ final byte[] additionalData = null;
+
+ final long fileSize = apkContent.size();
+
+ // Obtaining the strongest supported digest for each of the v2/v3/v3.1 blocks
+ // (CHUNKED_SHA256 or CHUNKED_SHA512).
+ final Map<Integer, byte[]> apkDigests = getApkDigests(apkContent);
+
+ // Obtaining the merkle tree and the root hash in verity format.
+ ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo =
+ ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
+
+ final ContentDigestAlgorithm verityContentDigestAlgorithm =
+ verityContentDigestInfo.contentDigestAlgorithm;
+ final byte[] rootHash = verityContentDigestInfo.rootHash;
+ final byte[] tree = verityContentDigestInfo.tree;
+
+ final Pair<Integer, Byte> hashingAlgorithmBlockSizePair = convertToV4HashingInfo(
+ verityContentDigestAlgorithm);
+ final V4Signature.HashingInfo hashingInfo = new V4Signature.HashingInfo(
+ hashingAlgorithmBlockSizePair.getFirst(), hashingAlgorithmBlockSizePair.getSecond(),
+ salt, rootHash);
+
+ // Generating SigningInfo and combining everything into V4Signature.
+ final V4Signature signature;
+ try {
+ signature = generateSignature(signerConfig, hashingInfo, apkDigests, additionalData,
+ fileSize);
+ } catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
+ throw new InvalidKeyException("Signer failed", e);
+ }
+
+ return Pair.of(signature, tree);
+ }
+
+ private static V4Signature.SigningInfo generateSigningInfo(
+ ApkSigningBlockUtils.SignerConfig signerConfig,
+ V4Signature.HashingInfo hashingInfo,
+ byte[] apkDigest, byte[] additionalData, long fileSize)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
+ CertificateEncodingException {
+ if (signerConfig.certificates.isEmpty()) {
+ throw new SignatureException("No certificates configured for signer");
+ }
+ if (signerConfig.certificates.size() != 1) {
+ throw new CertificateEncodingException("Should only have one certificate");
+ }
+
+ // Collecting data for signing.
+ final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
+
+ final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates);
+ final byte[] encodedCertificate = encodedCertificates.get(0);
+
+ final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
+ encodedCertificate, additionalData, publicKey.getEncoded(), -1, null);
+
+ final byte[] data = V4Signature.getSignedData(fileSize, hashingInfo,
+ signingInfoNoSignature);
+
+ // Signing.
+ final List<Pair<Integer, byte[]>> signatures =
+ ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data);
+ if (signatures.size() != 1) {
+ throw new SignatureException("Should only be one signature generated");
+ }
+
+ final int signatureAlgorithmId = signatures.get(0).getFirst();
+ final byte[] signature = signatures.get(0).getSecond();
+
+ return new V4Signature.SigningInfo(apkDigest,
+ encodedCertificate, additionalData, publicKey.getEncoded(), signatureAlgorithmId,
+ signature);
+ }
+
+ private static V4Signature generateSignature(
+ SignerConfig signerConfig,
+ V4Signature.HashingInfo hashingInfo,
+ Map<Integer, byte[]> apkDigests, byte[] additionalData, long fileSize)
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
+ CertificateEncodingException {
+ byte[] apkDigest = apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V3)
+ ? apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V3)
+ : apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V2);
+ final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config,
+ hashingInfo, apkDigest, additionalData, fileSize);
+
+ final V4Signature.SigningInfos signingInfos;
+ if (signerConfig.v41Config != null) {
+ if (!apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V31)) {
+ throw new IllegalStateException(
+ "V4.1 cannot be signed without a V3.1 content digest");
+ }
+ apkDigest = apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V31);
+ final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock(
+ APK_SIGNATURE_SCHEME_V31_BLOCK_ID,
+ generateSigningInfo(signerConfig.v41Config, hashingInfo, apkDigest,
+ additionalData, fileSize).toByteArray());
+ signingInfos = new V4Signature.SigningInfos(signingInfo, extSigningBlock);
+ } else {
+ signingInfos = new V4Signature.SigningInfos(signingInfo);
+ }
+
+ return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
+ signingInfos.toByteArray());
+ }
+
+ /**
+ * Returns a {@code Map} from the APK signature scheme version to a {@code byte[]} of the
+ * strongest supported content digest found in that version's signature block for the V2,
+ * V3, and V3.1 signatures in the provided {@code apk}.
+ *
+ * <p>If a supported content digest algorithm is not found in any of the signature blocks,
+ * or if the APK is not signed by any of these signature schemes, then an {@code IOException}
+ * is thrown.
+ */
+ private static Map<Integer, byte[]> getApkDigests(DataSource apk) throws IOException {
+ ApkUtils.ZipSections zipSections;
+ try {
+ zipSections = ApkUtils.findZipSections(apk);
+ } catch (ZipFormatException e) {
+ throw new IOException("Malformed APK: not a ZIP archive", e);
+ }
+
+ Map<Integer, byte[]> sigSchemeToDigest = new HashMap<>(1);
+ try {
+ byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V31);
+ sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V31, digest);
+ } catch (SignatureException expected) {
+ // It is expected to catch a SignatureException if the APK does not have a v3.1
+ // signature.
+ }
+
+ SignatureException v3Exception = null;
+ try {
+ byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V3);
+ sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V3, digest);
+ } catch (SignatureException e) {
+ v3Exception = e;
+ }
+
+ SignatureException v2Exception = null;
+ try {
+ byte[] digest = getBestV2Digest(apk, zipSections);
+ sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V2, digest);
+ } catch (SignatureException e) {
+ v2Exception = e;
+ }
+
+ if (sigSchemeToDigest.size() > 0) {
+ return sigSchemeToDigest;
+ }
+
+ throw new IOException(
+ "Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: "
+ + v2Exception);
+ }
+
+ private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections,
+ int v3SchemeVersion) throws SignatureException {
+ final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+ final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ v3SchemeVersion);
+ final int blockId;
+ switch (v3SchemeVersion) {
+ case VERSION_APK_SIGNATURE_SCHEME_V31:
+ blockId = APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
+ break;
+ case VERSION_APK_SIGNATURE_SCHEME_V3:
+ blockId = APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid V3 scheme provided: " + v3SchemeVersion);
+ }
+ try {
+ final SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(apk, zipSections, blockId, result);
+ final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock;
+ V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify,
+ result);
+ } catch (Exception e) {
+ throw new SignatureException("Failed to extract and parse v3 block", e);
+ }
+
+ if (result.signers.size() != 1) {
+ throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
+ }
+
+ ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
+ if (signer.containsErrors()) {
+ throw new SignatureException("Parsing failed: " + signer.getErrors());
+ }
+
+ final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
+ result.signers.get(0).contentDigests;
+ return pickBestDigest(contentDigests);
+ }
+
+ private static byte[] getBestV2Digest(DataSource apk, ApkUtils.ZipSections zipSections)
+ throws SignatureException {
+ final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+ final Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
+ final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
+ try {
+ final SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(apk, zipSections,
+ APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result);
+ final ByteBuffer apkSignatureSchemeV2Block = signatureInfo.signatureBlock;
+ V2SchemeVerifier.parseSigners(
+ apkSignatureSchemeV2Block,
+ contentDigestsToVerify,
+ Collections.emptyMap(),
+ foundApkSigSchemeIds,
+ Integer.MAX_VALUE,
+ Integer.MAX_VALUE,
+ result);
+ } catch (Exception e) {
+ throw new SignatureException("Failed to extract and parse v2 block", e);
+ }
+
+ if (result.signers.size() != 1) {
+ throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
+ }
+
+ ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
+ if (signer.containsErrors()) {
+ throw new SignatureException("Parsing failed: " + signer.getErrors());
+ }
+
+ final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
+ signer.contentDigests;
+ return pickBestDigest(contentDigests);
+ }
+
+ private static byte[] pickBestDigest(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) throws SignatureException {
+ if (contentDigests == null || contentDigests.isEmpty()) {
+ throw new SignatureException("Should have at least one digest");
+ }
+
+ int bestAlgorithmOrder = -1;
+ byte[] bestDigest = null;
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
+ final SignatureAlgorithm signatureAlgorithm =
+ SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
+ final ContentDigestAlgorithm contentDigestAlgorithm =
+ signatureAlgorithm.getContentDigestAlgorithm();
+ if (!isSupported(contentDigestAlgorithm, true)) {
+ continue;
+ }
+ final int algorithmOrder = digestAlgorithmSortingOrder(contentDigestAlgorithm);
+ if (bestAlgorithmOrder < algorithmOrder) {
+ bestAlgorithmOrder = algorithmOrder;
+ bestDigest = contentDigest.getValue();
+ }
+ }
+ if (bestDigest == null) {
+ throw new SignatureException("Failed to find a supported digest in the source APK");
+ }
+ return bestDigest;
+ }
+
+ public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) {
+ switch (contentDigestAlgorithm) {
+ case CHUNKED_SHA256:
+ return 0;
+ case VERITY_CHUNKED_SHA256:
+ return 1;
+ case CHUNKED_SHA512:
+ return 2;
+ default:
+ return -1;
+ }
+ }
+
+ private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm,
+ boolean forV3Digest) {
+ if (contentDigestAlgorithm == null) {
+ return false;
+ }
+ if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
+ || contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512
+ || (forV3Digest
+ && contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
+ return true;
+ }
+ return false;
+ }
+
+ private static Pair<Integer, Byte> convertToV4HashingInfo(ContentDigestAlgorithm algorithm)
+ throws NoSuchAlgorithmException {
+ switch (algorithm) {
+ case VERITY_CHUNKED_SHA256:
+ return Pair.of(V4Signature.HASHING_ALGORITHM_SHA256,
+ V4Signature.LOG2_BLOCK_SIZE_4096_BYTES);
+ default:
+ throw new NoSuchAlgorithmException(
+ "Invalid hash algorithm, only SHA2-256 over 4 KB chunks supported.");
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
new file mode 100644
index 0000000000..c0a9013624
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v4;
+
+import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
+
+import com.android.apksig.ApkVerifier;
+import com.android.apksig.ApkVerifier.Issue;
+import com.android.apksig.internal.apk.ApkSigningBlockUtils;
+import com.android.apksig.internal.apk.ContentDigestAlgorithm;
+import com.android.apksig.internal.apk.SignatureAlgorithm;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.util.DataSource;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
+
+/**
+ * APK Signature Scheme V4 verifier.
+ * <p>
+ * Verifies the serialized V4Signature file against an APK.
+ */
+public abstract class V4SchemeVerifier {
+ /**
+ * Hidden constructor to prevent instantiation.
+ */
+ private V4SchemeVerifier() {
+ }
+
+ /**
+ * <p>
+ * The main goals of the verifier are: 1) parse V4Signature file fields 2) verifies the PKCS7
+ * signature block against the raw root hash bytes in the proto field 3) verifies that the raw
+ * root hash matches with the actual hash tree root of the give APK 4) if the file contains a
+ * verity tree, verifies that it matches with the actual verity tree computed from the given
+ * APK.
+ * </p>
+ */
+ public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile)
+ throws IOException, NoSuchAlgorithmException {
+ final V4Signature signature;
+ final byte[] tree;
+ try (InputStream input = new FileInputStream(v4SignatureFile)) {
+ signature = V4Signature.readFrom(input);
+ tree = V4Signature.readBytes(input);
+ }
+
+ final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
+ ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
+
+ if (signature == null) {
+ result.addError(Issue.V4_SIG_NO_SIGNATURES,
+ "Signature file does not contain a v4 signature.");
+ return result;
+ }
+
+ if (signature.version != V4Signature.CURRENT_VERSION) {
+ result.addWarning(Issue.V4_SIG_VERSION_NOT_CURRENT, signature.version,
+ V4Signature.CURRENT_VERSION);
+ }
+
+ V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
+ signature.hashingInfo);
+
+ V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray(
+ signature.signingInfos);
+
+ final ApkSigningBlockUtils.Result.SignerInfo signerInfo;
+
+ // Verify the primary signature over signedData.
+ {
+ V4Signature.SigningInfo signingInfo = signingInfos.signingInfo;
+ final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
+ signingInfo);
+ signerInfo = parseAndVerifySignatureBlock(signingInfo, signedData);
+ result.signers.add(signerInfo);
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+
+ // Verify all subsequent signatures.
+ for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) {
+ V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
+ signingInfoBlock.signingInfo);
+ final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
+ signingInfo);
+ result.signers.add(parseAndVerifySignatureBlock(signingInfo, signedData));
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
+
+ // Check if the root hash and the tree are correct.
+ verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
+ if (!result.containsErrors()) {
+ result.verified = true;
+ }
+
+ return result;
+ }
+
+ /**
+ * Parses the provided signature block and populates the {@code result}.
+ * <p>
+ * This verifies {@signingInfo} over {@code signedData}, as well as parsing the certificate
+ * contained in the signature block. This method adds one or more errors to the {@code result}.
+ */
+ private static ApkSigningBlockUtils.Result.SignerInfo parseAndVerifySignatureBlock(
+ V4Signature.SigningInfo signingInfo,
+ final byte[] signedData) throws NoSuchAlgorithmException {
+ final ApkSigningBlockUtils.Result.SignerInfo result =
+ new ApkSigningBlockUtils.Result.SignerInfo();
+ result.index = 0;
+
+ final int sigAlgorithmId = signingInfo.signatureAlgorithmId;
+ final byte[] sigBytes = signingInfo.signature;
+ result.signatures.add(
+ new ApkSigningBlockUtils.Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
+
+ SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
+ if (signatureAlgorithm == null) {
+ result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+ return result;
+ }
+
+ String jcaSignatureAlgorithm =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams =
+ signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
+
+ String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
+
+ final byte[] publicKeyBytes = signingInfo.publicKey;
+ PublicKey publicKey;
+ try {
+ publicKey = KeyFactory.getInstance(keyAlgorithm).generatePublic(
+ new X509EncodedKeySpec(publicKeyBytes));
+ } catch (Exception e) {
+ result.addError(Issue.V4_SIG_MALFORMED_PUBLIC_KEY, e);
+ return result;
+ }
+
+ try {
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(signedData);
+ if (!sig.verify(sigBytes)) {
+ result.addError(Issue.V4_SIG_DID_NOT_VERIFY, signatureAlgorithm);
+ return result;
+ }
+ result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException
+ | SignatureException e) {
+ result.addError(Issue.V4_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
+ return result;
+ }
+
+ if (signingInfo.certificate == null) {
+ result.addError(Issue.V4_SIG_NO_CERTIFICATE);
+ return result;
+ }
+
+ final X509Certificate certificate;
+ try {
+ // Wrap the cert so that the result's getEncoded returns exactly the original encoded
+ // form. Without this, getEncoded may return a different form from what was stored in
+ // the signature. This is because some X509Certificate(Factory) implementations
+ // re-encode certificates.
+ certificate = new GuaranteedEncodedFormX509Certificate(
+ X509CertificateUtils.generateCertificate(signingInfo.certificate),
+ signingInfo.certificate);
+ } catch (CertificateException e) {
+ result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE, e);
+ return result;
+ }
+ result.certs.add(certificate);
+
+ byte[] certificatePublicKeyBytes;
+ try {
+ certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
+ certificate.getPublicKey());
+ } catch (InvalidKeyException e) {
+ System.out.println("Caught an exception encoding the public key: " + e);
+ e.printStackTrace();
+ certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
+ }
+ if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+ result.addError(
+ Issue.V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+ ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
+ ApkSigningBlockUtils.toHex(publicKeyBytes));
+ return result;
+ }
+
+ // Add apk digest from the file to the result.
+ ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest =
+ new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
+ 0 /* signature algorithm id doesn't matter here */, signingInfo.apkDigest);
+ result.contentDigests.add(contentDigest);
+
+ return result;
+ }
+
+ private static void verifyRootHashAndTree(DataSource apkContent,
+ ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest,
+ byte[] expectedTree) throws IOException, NoSuchAlgorithmException {
+ ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo =
+ ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
+
+ ContentDigestAlgorithm algorithm = actualContentDigestInfo.contentDigestAlgorithm;
+ final byte[] actualDigest = actualContentDigestInfo.rootHash;
+ final byte[] actualTree = actualContentDigestInfo.tree;
+
+ if (!Arrays.equals(expectedDigest, actualDigest)) {
+ signerInfo.addError(
+ ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
+ algorithm,
+ toHex(expectedDigest),
+ toHex(actualDigest));
+ return;
+ }
+ // Only check verity tree if it is not empty
+ if (expectedTree != null && !Arrays.equals(expectedTree, actualTree)) {
+ signerInfo.addError(
+ ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
+ algorithm,
+ toHex(expectedDigest),
+ toHex(actualDigest));
+ return;
+ }
+
+ signerInfo.verifiedContentDigests.put(algorithm, actualDigest);
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
new file mode 100644
index 0000000000..1eac5a2604
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.apk.v4;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class V4Signature {
+ public static final int CURRENT_VERSION = 2;
+
+ public static final int HASHING_ALGORITHM_SHA256 = 1;
+ public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
+
+ public static final int MAX_SIGNING_INFOS_SIZE = 7168;
+
+ public static class HashingInfo {
+ public final int hashAlgorithm; // only 1 == SHA256 supported
+ public final byte log2BlockSize; // only 12 (block size 4096) supported now
+ public final byte[] salt; // used exactly as in fs-verity, 32 bytes max
+ public final byte[] rawRootHash; // salted digest of the first Merkle tree page
+
+ HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) {
+ this.hashAlgorithm = hashAlgorithm;
+ this.log2BlockSize = log2BlockSize;
+ this.salt = salt;
+ this.rawRootHash = rawRootHash;
+ }
+
+ static HashingInfo fromByteArray(byte[] bytes) throws IOException {
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+ final int hashAlgorithm = buffer.getInt();
+ final byte log2BlockSize = buffer.get();
+ byte[] salt = readBytes(buffer);
+ byte[] rawRootHash = readBytes(buffer);
+ return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash);
+ }
+
+ byte[] toByteArray() {
+ final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt)
+ + bytesSize(this.rawRootHash);
+ ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.putInt(this.hashAlgorithm);
+ buffer.put(this.log2BlockSize);
+ writeBytes(buffer, this.salt);
+ writeBytes(buffer, this.rawRootHash);
+ return buffer.array();
+ }
+ }
+
+ public static class SigningInfo {
+ public final byte[] apkDigest; // used to match with the corresponding APK
+ public final byte[] certificate; // ASN.1 DER form
+ public final byte[] additionalData; // a free-form binary data blob
+ public final byte[] publicKey; // ASN.1 DER, must match the certificate
+ public final int signatureAlgorithmId; // see the APK v2 doc for the list
+ public final byte[] signature;
+
+ SigningInfo(byte[] apkDigest, byte[] certificate, byte[] additionalData,
+ byte[] publicKey, int signatureAlgorithmId, byte[] signature) {
+ this.apkDigest = apkDigest;
+ this.certificate = certificate;
+ this.additionalData = additionalData;
+ this.publicKey = publicKey;
+ this.signatureAlgorithmId = signatureAlgorithmId;
+ this.signature = signature;
+ }
+
+ static SigningInfo fromByteArray(byte[] bytes) throws IOException {
+ return fromByteBuffer(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN));
+ }
+
+ static SigningInfo fromByteBuffer(ByteBuffer buffer) throws IOException {
+ byte[] apkDigest = readBytes(buffer);
+ byte[] certificate = readBytes(buffer);
+ byte[] additionalData = readBytes(buffer);
+ byte[] publicKey = readBytes(buffer);
+ int signatureAlgorithmId = buffer.getInt();
+ byte[] signature = readBytes(buffer);
+ return new SigningInfo(apkDigest, certificate, additionalData, publicKey,
+ signatureAlgorithmId, signature);
+ }
+
+ byte[] toByteArray() {
+ final int size = bytesSize(this.apkDigest) + bytesSize(this.certificate) + bytesSize(
+ this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/
+ + bytesSize(this.signature);
+ ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ writeBytes(buffer, this.apkDigest);
+ writeBytes(buffer, this.certificate);
+ writeBytes(buffer, this.additionalData);
+ writeBytes(buffer, this.publicKey);
+ buffer.putInt(this.signatureAlgorithmId);
+ writeBytes(buffer, this.signature);
+ return buffer.array();
+ }
+ }
+
+ public static class SigningInfoBlock {
+ public final int blockId;
+ public final byte[] signingInfo;
+
+ public SigningInfoBlock(int blockId, byte[] signingInfo) {
+ this.blockId = blockId;
+ this.signingInfo = signingInfo;
+ }
+
+ static SigningInfoBlock fromByteBuffer(ByteBuffer buffer) throws IOException {
+ int blockId = buffer.getInt();
+ byte[] signingInfo = readBytes(buffer);
+ return new SigningInfoBlock(blockId, signingInfo);
+ }
+
+ byte[] toByteArray() {
+ final int size = 4/*blockId*/ + bytesSize(this.signingInfo);
+ ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.putInt(this.blockId);
+ writeBytes(buffer, this.signingInfo);
+ return buffer.array();
+ }
+ }
+
+ public static class SigningInfos {
+ public final SigningInfo signingInfo;
+ public final SigningInfoBlock[] signingInfoBlocks;
+
+ public SigningInfos(SigningInfo signingInfo) {
+ this.signingInfo = signingInfo;
+ this.signingInfoBlocks = new SigningInfoBlock[0];
+ }
+
+ public SigningInfos(SigningInfo signingInfo, SigningInfoBlock... signingInfoBlocks) {
+ this.signingInfo = signingInfo;
+ this.signingInfoBlocks = signingInfoBlocks;
+ }
+
+ public static SigningInfos fromByteArray(byte[] bytes) throws IOException {
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+ SigningInfo signingInfo = SigningInfo.fromByteBuffer(buffer);
+ if (!buffer.hasRemaining()) {
+ return new SigningInfos(signingInfo);
+ }
+ ArrayList<SigningInfoBlock> signingInfoBlocks = new ArrayList<>(1);
+ while (buffer.hasRemaining()) {
+ signingInfoBlocks.add(SigningInfoBlock.fromByteBuffer(buffer));
+ }
+ return new SigningInfos(signingInfo,
+ signingInfoBlocks.toArray(new SigningInfoBlock[signingInfoBlocks.size()]));
+ }
+
+ byte[] toByteArray() {
+ byte[][] arrays = new byte[1 + this.signingInfoBlocks.length][];
+ arrays[0] = this.signingInfo.toByteArray();
+ int size = arrays[0].length;
+ for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
+ arrays[i + 1] = this.signingInfoBlocks[i].toByteArray();
+ size += arrays[i + 1].length;
+ }
+ if (size > MAX_SIGNING_INFOS_SIZE) {
+ throw new IllegalArgumentException(
+ "Combined SigningInfos length exceeded limit of 7K: " + size);
+ }
+
+ // Combine all arrays into one.
+ byte[] result = Arrays.copyOf(arrays[0], size);
+ int offset = arrays[0].length;
+ for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
+ System.arraycopy(arrays[i + 1], 0, result, offset, arrays[i + 1].length);
+ offset += arrays[i + 1].length;
+ }
+ return result;
+ }
+ }
+
+ // Always 2 for now.
+ public final int version;
+ public final byte[] hashingInfo;
+ // Can contain either SigningInfo or SigningInfo + one or multiple SigningInfoBlock.
+ // Passed as-is to the kernel. Can be retrieved later.
+ public final byte[] signingInfos;
+
+ V4Signature(int version, byte[] hashingInfo, byte[] signingInfos) {
+ this.version = version;
+ this.hashingInfo = hashingInfo;
+ this.signingInfos = signingInfos;
+ }
+
+ static V4Signature readFrom(InputStream stream) throws IOException {
+ final int version = readIntLE(stream);
+ if (version != CURRENT_VERSION) {
+ throw new IOException("Invalid signature version.");
+ }
+ final byte[] hashingInfo = readBytes(stream);
+ final byte[] signingInfo = readBytes(stream);
+ return new V4Signature(version, hashingInfo, signingInfo);
+ }
+
+ public void writeTo(OutputStream stream) throws IOException {
+ writeIntLE(stream, this.version);
+ writeBytes(stream, this.hashingInfo);
+ writeBytes(stream, this.signingInfos);
+ }
+
+ static byte[] getSignedData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
+ final int size =
+ 4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize(
+ hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize(
+ signingInfo.apkDigest) + bytesSize(signingInfo.certificate) + bytesSize(
+ signingInfo.additionalData);
+ ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.putInt(size);
+ buffer.putLong(fileSize);
+ buffer.putInt(hashingInfo.hashAlgorithm);
+ buffer.put(hashingInfo.log2BlockSize);
+ writeBytes(buffer, hashingInfo.salt);
+ writeBytes(buffer, hashingInfo.rawRootHash);
+ writeBytes(buffer, signingInfo.apkDigest);
+ writeBytes(buffer, signingInfo.certificate);
+ writeBytes(buffer, signingInfo.additionalData);
+ return buffer.array();
+ }
+
+ // Utility methods.
+ static int bytesSize(byte[] bytes) {
+ return 4/*length*/ + (bytes == null ? 0 : bytes.length);
+ }
+
+ static void readFully(InputStream stream, byte[] buffer) throws IOException {
+ int len = buffer.length;
+ int n = 0;
+ while (n < len) {
+ int count = stream.read(buffer, n, len - n);
+ if (count < 0) {
+ throw new EOFException();
+ }
+ n += count;
+ }
+ }
+
+ static int readIntLE(InputStream stream) throws IOException {
+ final byte[] buffer = new byte[4];
+ readFully(stream, buffer);
+ return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
+ }
+
+ static void writeIntLE(OutputStream stream, int v) throws IOException {
+ final byte[] buffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
+ stream.write(buffer);
+ }
+
+ static byte[] readBytes(InputStream stream) throws IOException {
+ try {
+ final int size = readIntLE(stream);
+ final byte[] bytes = new byte[size];
+ readFully(stream, bytes);
+ return bytes;
+ } catch (EOFException ignored) {
+ return null;
+ }
+ }
+
+ static byte[] readBytes(ByteBuffer buffer) throws IOException {
+ if (buffer.remaining() < 4) {
+ throw new EOFException();
+ }
+ final int size = buffer.getInt();
+ if (buffer.remaining() < size) {
+ throw new EOFException();
+ }
+ final byte[] bytes = new byte[size];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ static void writeBytes(OutputStream stream, byte[] bytes) throws IOException {
+ if (bytes == null) {
+ writeIntLE(stream, 0);
+ return;
+ }
+ writeIntLE(stream, bytes.length);
+ stream.write(bytes);
+ }
+
+ static void writeBytes(ByteBuffer buffer, byte[] bytes) {
+ if (bytes == null) {
+ buffer.putInt(0);
+ return;
+ }
+ buffer.putInt(bytes.length);
+ buffer.put(bytes);
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
new file mode 100644
index 0000000000..160dc4e233
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
@@ -0,0 +1,673 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+import com.android.apksig.internal.asn1.ber.BerDataValue;
+import com.android.apksig.internal.asn1.ber.BerDataValueFormatException;
+import com.android.apksig.internal.asn1.ber.BerDataValueReader;
+import com.android.apksig.internal.asn1.ber.BerEncoding;
+import com.android.apksig.internal.asn1.ber.ByteBufferBerDataValueReader;
+import com.android.apksig.internal.util.ByteBufferUtils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parser of ASN.1 BER-encoded structures.
+ *
+ * <p>Structure is described to the parser by providing a class annotated with {@link Asn1Class},
+ * containing fields annotated with {@link Asn1Field}.
+ */
+public final class Asn1BerParser {
+ private Asn1BerParser() {}
+
+ /**
+ * Returns the ASN.1 structure contained in the BER encoded input.
+ *
+ * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
+ * is advanced to the first position following the end of the consumed structure.
+ * @param containerClass class describing the structure of the input. The class must meet the
+ * following requirements:
+ * <ul>
+ * <li>The class must be annotated with {@link Asn1Class}.</li>
+ * <li>The class must expose a public no-arg constructor.</li>
+ * <li>Member fields of the class which are populated with parsed input must be
+ * annotated with {@link Asn1Field} and be public and non-final.</li>
+ * </ul>
+ *
+ * @throws Asn1DecodingException if the input could not be decoded into the specified Java
+ * object
+ */
+ public static <T> T parse(ByteBuffer encoded, Class<T> containerClass)
+ throws Asn1DecodingException {
+ BerDataValue containerDataValue;
+ try {
+ containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
+ } catch (BerDataValueFormatException e) {
+ throw new Asn1DecodingException("Failed to decode top-level data value", e);
+ }
+ if (containerDataValue == null) {
+ throw new Asn1DecodingException("Empty input");
+ }
+ return parse(containerDataValue, containerClass);
+ }
+
+ /**
+ * Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means
+ * that this method does not care whether the tag number of this data structure is
+ * {@code SET OF} and whether the tag class is {@code UNIVERSAL}.
+ *
+ * <p>Note: The returned type is {@link List} rather than {@link java.util.Set} because ASN.1
+ * SET may contain duplicate elements.
+ *
+ * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
+ * is advanced to the first position following the end of the consumed structure.
+ * @param elementClass class describing the structure of the values/elements contained in this
+ * container. The class must meet the following requirements:
+ * <ul>
+ * <li>The class must be annotated with {@link Asn1Class}.</li>
+ * <li>The class must expose a public no-arg constructor.</li>
+ * <li>Member fields of the class which are populated with parsed input must be
+ * annotated with {@link Asn1Field} and be public and non-final.</li>
+ * </ul>
+ *
+ * @throws Asn1DecodingException if the input could not be decoded into the specified Java
+ * object
+ */
+ public static <T> List<T> parseImplicitSetOf(ByteBuffer encoded, Class<T> elementClass)
+ throws Asn1DecodingException {
+ BerDataValue containerDataValue;
+ try {
+ containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
+ } catch (BerDataValueFormatException e) {
+ throw new Asn1DecodingException("Failed to decode top-level data value", e);
+ }
+ if (containerDataValue == null) {
+ throw new Asn1DecodingException("Empty input");
+ }
+ return parseSetOf(containerDataValue, elementClass);
+ }
+
+ private static <T> T parse(BerDataValue container, Class<T> containerClass)
+ throws Asn1DecodingException {
+ if (container == null) {
+ throw new NullPointerException("container == null");
+ }
+ if (containerClass == null) {
+ throw new NullPointerException("containerClass == null");
+ }
+
+ Asn1Type dataType = getContainerAsn1Type(containerClass);
+ switch (dataType) {
+ case CHOICE:
+ return parseChoice(container, containerClass);
+
+ case SEQUENCE:
+ {
+ int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL;
+ int expectedTagNumber = BerEncoding.getTagNumber(dataType);
+ if ((container.getTagClass() != expectedTagClass)
+ || (container.getTagNumber() != expectedTagNumber)) {
+ throw new Asn1UnexpectedTagException(
+ "Unexpected data value read as " + containerClass.getName()
+ + ". Expected " + BerEncoding.tagClassAndNumberToString(
+ expectedTagClass, expectedTagNumber)
+ + ", but read: " + BerEncoding.tagClassAndNumberToString(
+ container.getTagClass(), container.getTagNumber()));
+ }
+ return parseSequence(container, containerClass);
+ }
+ case UNENCODED_CONTAINER:
+ return parseSequence(container, containerClass, true);
+ default:
+ throw new Asn1DecodingException("Parsing container " + dataType + " not supported");
+ }
+ }
+
+ private static <T> T parseChoice(BerDataValue dataValue, Class<T> containerClass)
+ throws Asn1DecodingException {
+ List<AnnotatedField> fields = getAnnotatedFields(containerClass);
+ if (fields.isEmpty()) {
+ throw new Asn1DecodingException(
+ "No fields annotated with " + Asn1Field.class.getName()
+ + " in CHOICE class " + containerClass.getName());
+ }
+
+ // Check that class + tagNumber don't clash between the choices
+ for (int i = 0; i < fields.size() - 1; i++) {
+ AnnotatedField f1 = fields.get(i);
+ int tagNumber1 = f1.getBerTagNumber();
+ int tagClass1 = f1.getBerTagClass();
+ for (int j = i + 1; j < fields.size(); j++) {
+ AnnotatedField f2 = fields.get(j);
+ int tagNumber2 = f2.getBerTagNumber();
+ int tagClass2 = f2.getBerTagClass();
+ if ((tagNumber1 == tagNumber2) && (tagClass1 == tagClass2)) {
+ throw new Asn1DecodingException(
+ "CHOICE fields are indistinguishable because they have the same tag"
+ + " class and number: " + containerClass.getName()
+ + "." + f1.getField().getName()
+ + " and ." + f2.getField().getName());
+ }
+ }
+ }
+
+ // Instantiate the container object / result
+ T obj;
+ try {
+ obj = containerClass.getConstructor().newInstance();
+ } catch (IllegalArgumentException | ReflectiveOperationException e) {
+ throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
+ }
+ // Set the matching field's value from the data value
+ for (AnnotatedField field : fields) {
+ try {
+ field.setValueFrom(dataValue, obj);
+ return obj;
+ } catch (Asn1UnexpectedTagException expected) {
+ // not a match
+ }
+ }
+
+ throw new Asn1DecodingException(
+ "No options of CHOICE " + containerClass.getName() + " matched");
+ }
+
+ private static <T> T parseSequence(BerDataValue container, Class<T> containerClass)
+ throws Asn1DecodingException {
+ return parseSequence(container, containerClass, false);
+ }
+
+ private static <T> T parseSequence(BerDataValue container, Class<T> containerClass,
+ boolean isUnencodedContainer) throws Asn1DecodingException {
+ List<AnnotatedField> fields = getAnnotatedFields(containerClass);
+ Collections.sort(
+ fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
+ // Check that there are no fields with the same index
+ if (fields.size() > 1) {
+ AnnotatedField lastField = null;
+ for (AnnotatedField field : fields) {
+ if ((lastField != null)
+ && (lastField.getAnnotation().index() == field.getAnnotation().index())) {
+ throw new Asn1DecodingException(
+ "Fields have the same index: " + containerClass.getName()
+ + "." + lastField.getField().getName()
+ + " and ." + field.getField().getName());
+ }
+ lastField = field;
+ }
+ }
+
+ // Instantiate the container object / result
+ T t;
+ try {
+ t = containerClass.getConstructor().newInstance();
+ } catch (IllegalArgumentException | ReflectiveOperationException e) {
+ throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
+ }
+
+ // Parse fields one by one. A complication is that there may be optional fields.
+ int nextUnreadFieldIndex = 0;
+ BerDataValueReader elementsReader = container.contentsReader();
+ while (nextUnreadFieldIndex < fields.size()) {
+ BerDataValue dataValue;
+ try {
+ // if this is the first field of an unencoded container then the entire contents of
+ // the container should be used when assigning to this field.
+ if (isUnencodedContainer && nextUnreadFieldIndex == 0) {
+ dataValue = container;
+ } else {
+ dataValue = elementsReader.readDataValue();
+ }
+ } catch (BerDataValueFormatException e) {
+ throw new Asn1DecodingException("Malformed data value", e);
+ }
+ if (dataValue == null) {
+ break;
+ }
+
+ for (int i = nextUnreadFieldIndex; i < fields.size(); i++) {
+ AnnotatedField field = fields.get(i);
+ try {
+ if (field.isOptional()) {
+ // Optional field -- might not be present and we may thus be trying to set
+ // it from the wrong tag.
+ try {
+ field.setValueFrom(dataValue, t);
+ nextUnreadFieldIndex = i + 1;
+ break;
+ } catch (Asn1UnexpectedTagException e) {
+ // This field is not present, attempt to use this data value for the
+ // next / iteration of the loop
+ continue;
+ }
+ } else {
+ // Mandatory field -- if we can't set its value from this data value, then
+ // it's an error
+ field.setValueFrom(dataValue, t);
+ nextUnreadFieldIndex = i + 1;
+ break;
+ }
+ } catch (Asn1DecodingException e) {
+ throw new Asn1DecodingException(
+ "Failed to parse " + containerClass.getName()
+ + "." + field.getField().getName(),
+ e);
+ }
+ }
+ }
+
+ return t;
+ }
+
+ // NOTE: This method returns List rather than Set because ASN.1 SET_OF does require uniqueness
+ // of elements -- it's an unordered collection.
+ @SuppressWarnings("unchecked")
+ private static <T> List<T> parseSetOf(BerDataValue container, Class<T> elementClass)
+ throws Asn1DecodingException {
+ List<T> result = new ArrayList<>();
+ BerDataValueReader elementsReader = container.contentsReader();
+ while (true) {
+ BerDataValue dataValue;
+ try {
+ dataValue = elementsReader.readDataValue();
+ } catch (BerDataValueFormatException e) {
+ throw new Asn1DecodingException("Malformed data value", e);
+ }
+ if (dataValue == null) {
+ break;
+ }
+ T element;
+ if (ByteBuffer.class.equals(elementClass)) {
+ element = (T) dataValue.getEncodedContents();
+ } else if (Asn1OpaqueObject.class.equals(elementClass)) {
+ element = (T) new Asn1OpaqueObject(dataValue.getEncoded());
+ } else {
+ element = parse(dataValue, elementClass);
+ }
+ result.add(element);
+ }
+ return result;
+ }
+
+ private static Asn1Type getContainerAsn1Type(Class<?> containerClass)
+ throws Asn1DecodingException {
+ Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
+ if (containerAnnotation == null) {
+ throw new Asn1DecodingException(
+ containerClass.getName() + " is not annotated with "
+ + Asn1Class.class.getName());
+ }
+
+ switch (containerAnnotation.type()) {
+ case CHOICE:
+ case SEQUENCE:
+ case UNENCODED_CONTAINER:
+ return containerAnnotation.type();
+ default:
+ throw new Asn1DecodingException(
+ "Unsupported ASN.1 container annotation type: "
+ + containerAnnotation.type());
+ }
+ }
+
+ private static Class<?> getElementType(Field field)
+ throws Asn1DecodingException, ClassNotFoundException {
+ String type = field.getGenericType().getTypeName();
+ int delimiterIndex = type.indexOf('<');
+ if (delimiterIndex == -1) {
+ throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
+ }
+ int startIndex = delimiterIndex + 1;
+ int endIndex = type.indexOf('>', startIndex);
+ // TODO: handle comma?
+ if (endIndex == -1) {
+ throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
+ }
+ String elementClassName = type.substring(startIndex, endIndex);
+ return Class.forName(elementClassName);
+ }
+
+ private static final class AnnotatedField {
+ private final Field mField;
+ private final Asn1Field mAnnotation;
+ private final Asn1Type mDataType;
+ private final Asn1TagClass mTagClass;
+ private final int mBerTagClass;
+ private final int mBerTagNumber;
+ private final Asn1Tagging mTagging;
+ private final boolean mOptional;
+
+ public AnnotatedField(Field field, Asn1Field annotation) throws Asn1DecodingException {
+ mField = field;
+ mAnnotation = annotation;
+ mDataType = annotation.type();
+
+ Asn1TagClass tagClass = annotation.cls();
+ if (tagClass == Asn1TagClass.AUTOMATIC) {
+ if (annotation.tagNumber() != -1) {
+ tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
+ } else {
+ tagClass = Asn1TagClass.UNIVERSAL;
+ }
+ }
+ mTagClass = tagClass;
+ mBerTagClass = BerEncoding.getTagClass(mTagClass);
+
+ int tagNumber;
+ if (annotation.tagNumber() != -1) {
+ tagNumber = annotation.tagNumber();
+ } else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
+ tagNumber = -1;
+ } else {
+ tagNumber = BerEncoding.getTagNumber(mDataType);
+ }
+ mBerTagNumber = tagNumber;
+
+ mTagging = annotation.tagging();
+ if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
+ && (annotation.tagNumber() == -1)) {
+ throw new Asn1DecodingException(
+ "Tag number must be specified when tagging mode is " + mTagging);
+ }
+
+ mOptional = annotation.optional();
+ }
+
+ public Field getField() {
+ return mField;
+ }
+
+ public Asn1Field getAnnotation() {
+ return mAnnotation;
+ }
+
+ public boolean isOptional() {
+ return mOptional;
+ }
+
+ public int getBerTagClass() {
+ return mBerTagClass;
+ }
+
+ public int getBerTagNumber() {
+ return mBerTagNumber;
+ }
+
+ public void setValueFrom(BerDataValue dataValue, Object obj) throws Asn1DecodingException {
+ int readTagClass = dataValue.getTagClass();
+ if (mBerTagNumber != -1) {
+ int readTagNumber = dataValue.getTagNumber();
+ if ((readTagClass != mBerTagClass) || (readTagNumber != mBerTagNumber)) {
+ throw new Asn1UnexpectedTagException(
+ "Tag mismatch. Expected: "
+ + BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber)
+ + ", but found "
+ + BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber));
+ }
+ } else {
+ if (readTagClass != mBerTagClass) {
+ throw new Asn1UnexpectedTagException(
+ "Tag mismatch. Expected class: "
+ + BerEncoding.tagClassToString(mBerTagClass)
+ + ", but found "
+ + BerEncoding.tagClassToString(readTagClass));
+ }
+ }
+
+ if (mTagging == Asn1Tagging.EXPLICIT) {
+ try {
+ dataValue = dataValue.contentsReader().readDataValue();
+ } catch (BerDataValueFormatException e) {
+ throw new Asn1DecodingException(
+ "Failed to read contents of EXPLICIT data value", e);
+ }
+ }
+
+ BerToJavaConverter.setFieldValue(obj, mField, mDataType, dataValue);
+ }
+ }
+
+ private static class Asn1UnexpectedTagException extends Asn1DecodingException {
+ private static final long serialVersionUID = 1L;
+
+ public Asn1UnexpectedTagException(String message) {
+ super(message);
+ }
+ }
+
+ private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException {
+ if (!encodedOid.hasRemaining()) {
+ throw new Asn1DecodingException("Empty OBJECT IDENTIFIER");
+ }
+
+ // First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2
+ long firstComponent = decodeBase128UnsignedLong(encodedOid);
+ int firstNode = (int) Math.min(firstComponent / 40, 2);
+ long secondNode = firstComponent - firstNode * 40;
+ StringBuilder result = new StringBuilder();
+ result.append(Long.toString(firstNode)).append('.')
+ .append(Long.toString(secondNode));
+
+ // Each consecutive node is encoded as a separate component
+ while (encodedOid.hasRemaining()) {
+ long node = decodeBase128UnsignedLong(encodedOid);
+ result.append('.').append(Long.toString(node));
+ }
+
+ return result.toString();
+ }
+
+ private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException {
+ if (!encoded.hasRemaining()) {
+ return 0;
+ }
+ long result = 0;
+ while (encoded.hasRemaining()) {
+ if (result > Long.MAX_VALUE >>> 7) {
+ throw new Asn1DecodingException("Base-128 number too large");
+ }
+ int b = encoded.get() & 0xff;
+ result <<= 7;
+ result |= b & 0x7f;
+ if ((b & 0x80) == 0) {
+ return result;
+ }
+ }
+ throw new Asn1DecodingException(
+ "Truncated base-128 encoded input: missing terminating byte, with highest bit not"
+ + " set");
+ }
+
+ private static BigInteger integerToBigInteger(ByteBuffer encoded) {
+ if (!encoded.hasRemaining()) {
+ return BigInteger.ZERO;
+ }
+ return new BigInteger(ByteBufferUtils.toByteArray(encoded));
+ }
+
+ private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException {
+ BigInteger value = integerToBigInteger(encoded);
+ if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
+ || value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
+ throw new Asn1DecodingException(
+ String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value));
+ }
+ return value.intValue();
+ }
+
+ private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException {
+ BigInteger value = integerToBigInteger(encoded);
+ if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0
+ || value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
+ throw new Asn1DecodingException(
+ String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value));
+ }
+ return value.longValue();
+ }
+
+ private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass)
+ throws Asn1DecodingException {
+ Field[] declaredFields = containerClass.getDeclaredFields();
+ List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
+ for (Field field : declaredFields) {
+ Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
+ if (annotation == null) {
+ continue;
+ }
+ if (Modifier.isStatic(field.getModifiers())) {
+ throw new Asn1DecodingException(
+ Asn1Field.class.getName() + " used on a static field: "
+ + containerClass.getName() + "." + field.getName());
+ }
+
+ AnnotatedField annotatedField;
+ try {
+ annotatedField = new AnnotatedField(field, annotation);
+ } catch (Asn1DecodingException e) {
+ throw new Asn1DecodingException(
+ "Invalid ASN.1 annotation on "
+ + containerClass.getName() + "." + field.getName(),
+ e);
+ }
+ result.add(annotatedField);
+ }
+ return result;
+ }
+
+ private static final class BerToJavaConverter {
+ private BerToJavaConverter() {}
+
+ public static void setFieldValue(
+ Object obj, Field field, Asn1Type type, BerDataValue dataValue)
+ throws Asn1DecodingException {
+ try {
+ switch (type) {
+ case SET_OF:
+ case SEQUENCE_OF:
+ if (Asn1OpaqueObject.class.equals(field.getType())) {
+ field.set(obj, convert(type, dataValue, field.getType()));
+ } else {
+ field.set(obj, parseSetOf(dataValue, getElementType(field)));
+ }
+ return;
+ default:
+ field.set(obj, convert(type, dataValue, field.getType()));
+ break;
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new Asn1DecodingException(
+ "Failed to set value of " + obj.getClass().getName()
+ + "." + field.getName(),
+ e);
+ }
+ }
+
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ @SuppressWarnings("unchecked")
+ public static <T> T convert(
+ Asn1Type sourceType,
+ BerDataValue dataValue,
+ Class<T> targetType) throws Asn1DecodingException {
+ if (ByteBuffer.class.equals(targetType)) {
+ return (T) dataValue.getEncodedContents();
+ } else if (byte[].class.equals(targetType)) {
+ ByteBuffer resultBuf = dataValue.getEncodedContents();
+ if (!resultBuf.hasRemaining()) {
+ return (T) EMPTY_BYTE_ARRAY;
+ }
+ byte[] result = new byte[resultBuf.remaining()];
+ resultBuf.get(result);
+ return (T) result;
+ } else if (Asn1OpaqueObject.class.equals(targetType)) {
+ return (T) new Asn1OpaqueObject(dataValue.getEncoded());
+ }
+ ByteBuffer encodedContents = dataValue.getEncodedContents();
+ switch (sourceType) {
+ case INTEGER:
+ if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) {
+ return (T) Integer.valueOf(integerToInt(encodedContents));
+ } else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) {
+ return (T) Long.valueOf(integerToLong(encodedContents));
+ } else if (BigInteger.class.equals(targetType)) {
+ return (T) integerToBigInteger(encodedContents);
+ }
+ break;
+ case OBJECT_IDENTIFIER:
+ if (String.class.equals(targetType)) {
+ return (T) oidToString(encodedContents);
+ }
+ break;
+ case UTC_TIME:
+ case GENERALIZED_TIME:
+ if (String.class.equals(targetType)) {
+ return (T) new String(ByteBufferUtils.toByteArray(encodedContents));
+ }
+ break;
+ case BOOLEAN:
+ // A boolean should be encoded in a single byte with a value of 0 for false and
+ // any non-zero value for true.
+ if (boolean.class.equals(targetType)) {
+ if (encodedContents.remaining() != 1) {
+ throw new Asn1DecodingException(
+ "Incorrect encoded size of boolean value: "
+ + encodedContents.remaining());
+ }
+ boolean result;
+ if (encodedContents.get() == 0) {
+ result = false;
+ } else {
+ result = true;
+ }
+ return (T) new Boolean(result);
+ }
+ break;
+ case SEQUENCE:
+ {
+ Asn1Class containerAnnotation =
+ targetType.getDeclaredAnnotation(Asn1Class.class);
+ if ((containerAnnotation != null)
+ && (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
+ return parseSequence(dataValue, targetType);
+ }
+ break;
+ }
+ case CHOICE:
+ {
+ Asn1Class containerAnnotation =
+ targetType.getDeclaredAnnotation(Asn1Class.class);
+ if ((containerAnnotation != null)
+ && (containerAnnotation.type() == Asn1Type.CHOICE)) {
+ return parseChoice(dataValue, targetType);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ throw new Asn1DecodingException(
+ "Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName());
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java
new file mode 100644
index 0000000000..4841296c6b
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java
new file mode 100644
index 0000000000..07886429bf
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
new file mode 100644
index 0000000000..901f5f30c0
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.asn1;
+
+import com.android.apksig.internal.asn1.ber.BerEncoding;
+
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Encoder of ASN.1 structures into DER-encoded form.
+ *
+ * <p>Structure is described to the encoder by providing a class annotated with {@link Asn1Class},
+ * containing fields annotated with {@link Asn1Field}.
+ */
+public final class Asn1DerEncoder {
+ private Asn1DerEncoder() {}
+
+ /**
+ * Returns the DER-encoded form of the provided ASN.1 structure.
+ *
+ * @param container container to be encoded. The container's class must meet the following
+ * requirements:
+ * <ul>
+ * <li>The class must be annotated with {@link Asn1Class}.</li>
+ * <li>Member fields of the class which are to be encoded must be annotated with
+ * {@link Asn1Field} and be public.</li>
+ * </ul>
+ *
+ * @throws Asn1EncodingException if the input could not be encoded
+ */
+ public static byte[] encode(Object container) throws Asn1EncodingException {
+ Class<?> containerClass = container.getClass();
+ Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
+ if (containerAnnotation == null) {
+ throw new Asn1EncodingException(
+ containerClass.getName() + " not annotated with " + Asn1Class.class.getName());
+ }
+
+ Asn1Type containerType = containerAnnotation.type();
+ switch (containerType) {
+ case CHOICE:
+ return toChoice(container);
+ case SEQUENCE:
+ return toSequence(container);
+ case UNENCODED_CONTAINER:
+ return toSequence(container, true);
+ default:
+ throw new Asn1EncodingException("Unsupported container type: " + containerType);
+ }
+ }
+
+ private static byte[] toChoice(Object container) throws Asn1EncodingException {
+ Class<?> containerClass = container.getClass();
+ List<AnnotatedField> fields = getAnnotatedFields(container);
+ if (fields.isEmpty()) {
+ throw new Asn1EncodingException(
+ "No fields annotated with " + Asn1Field.class.getName()
+ + " in CHOICE class " + containerClass.getName());
+ }
+
+ AnnotatedField resultField = null;
+ for (AnnotatedField field : fields) {
+ Object fieldValue = getMemberFieldValue(container, field.getField());
+ if (fieldValue != null) {
+ if (resultField != null) {
+ throw new Asn1EncodingException(
+ "Multiple non-null fields in CHOICE class " + containerClass.getName()
+ + ": " + resultField.getField().getName()
+ + ", " + field.getField().getName());
+ }
+ resultField = field;
+ }
+ }
+
+ if (resultField == null) {
+ throw new Asn1EncodingException(
+ "No non-null fields in CHOICE class " + containerClass.getName());
+ }
+
+ return resultField.toDer();
+ }
+
+ private static byte[] toSequence(Object container) throws Asn1EncodingException {
+ return toSequence(container, false);
+ }
+
+ private static byte[] toSequence(Object container, boolean omitTag)
+ throws Asn1EncodingException {
+ Class<?> containerClass = container.getClass();
+ List<AnnotatedField> fields = getAnnotatedFields(container);
+ Collections.sort(
+ fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
+ if (fields.size() > 1) {
+ AnnotatedField lastField = null;
+ for (AnnotatedField field : fields) {
+ if ((lastField != null)
+ && (lastField.getAnnotation().index() == field.getAnnotation().index())) {
+ throw new Asn1EncodingException(
+ "Fields have the same index: " + containerClass.getName()
+ + "." + lastField.getField().getName()
+ + " and ." + field.getField().getName());
+ }
+ lastField = field;
+ }
+ }
+
+ List<byte[]> serializedFields = new ArrayList<>(fields.size());
+ int contentLen = 0;
+ for (AnnotatedField field : fields) {
+ byte[] serializedField;
+ try {
+ serializedField = field.toDer();
+ } catch (Asn1EncodingException e) {
+ throw new Asn1EncodingException(
+ "Failed to encode " + containerClass.getName()
+ + "." + field.getField().getName(),
+ e);
+ }
+ if (serializedField != null) {
+ serializedFields.add(serializedField);
+ contentLen += serializedField.length;
+ }
+ }
+
+ if (omitTag) {
+ byte[] unencodedResult = new byte[contentLen];
+ int index = 0;
+ for (byte[] serializedField : serializedFields) {
+ System.arraycopy(serializedField, 0, unencodedResult, index, serializedField.length);
+ index += serializedField.length;
+ }
+ return unencodedResult;
+ } else {
+ return createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE,
+ serializedFields.toArray(new byte[0][]));
+ }
+ }
+
+ private static byte[] toSetOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
+ return toSequenceOrSetOf(values, elementType, true);
+ }
+
+ private static byte[] toSequenceOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
+ return toSequenceOrSetOf(values, elementType, false);
+ }
+
+ private static byte[] toSequenceOrSetOf(Collection<?> values, Asn1Type elementType, boolean toSet)
+ throws Asn1EncodingException {
+ List<byte[]> serializedValues = new ArrayList<>(values.size());
+ for (Object value : values) {
+ serializedValues.add(JavaToDerConverter.toDer(value, elementType, null));
+ }
+ int tagNumber;
+ if (toSet) {
+ if (serializedValues.size() > 1) {
+ Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE);
+ }
+ tagNumber = BerEncoding.TAG_NUMBER_SET;
+ } else {
+ tagNumber = BerEncoding.TAG_NUMBER_SEQUENCE;
+ }
+ return createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL, true, tagNumber,
+ serializedValues.toArray(new byte[0][]));
+ }
+
+ /**
+ * Compares two bytes arrays based on their lexicographic order. Corresponding elements of the
+ * two arrays are compared in ascending order. Elements at out of range indices are assumed to
+ * be smaller than the smallest possible value for an element.
+ */
+ private static class ByteArrayLexicographicComparator implements Comparator<byte[]> {
+ private static final ByteArrayLexicographicComparator INSTANCE =
+ new ByteArrayLexicographicComparator();
+
+ @Override
+ public int compare(byte[] arr1, byte[] arr2) {
+ int commonLength = Math.min(arr1.length, arr2.length);
+ for (int i = 0; i < commonLength; i++) {
+ int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff);
+ if (diff != 0) {
+ return diff;
+ }
+ }
+ return arr1.length - arr2.length;
+ }
+ }
+
+ private static List<AnnotatedField> getAnnotatedFields(Object container)
+ throws Asn1EncodingException {
+ Class<?> containerClass = container.getClass();
+ Field[] declaredFields = containerClass.getDeclaredFields();
+ List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
+ for (Field field : declaredFields) {
+ Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
+ if (annotation == null) {
+ continue;
+ }
+ if (Modifier.isStatic(field.getModifiers())) {
+ throw new Asn1EncodingException(
+ Asn1Field.class.getName() + " used on a static field: "
+ + containerClass.getName() + "." + field.getName());
+ }
+
+ AnnotatedField annotatedField;
+ try {
+ annotatedField = new AnnotatedField(container, field, annotation);
+ } catch (Asn1EncodingException e) {
+ throw new Asn1EncodingException(
+ "Invalid ASN.1 annotation on "
+ + containerClass.getName() + "." + field.getName(),
+ e);
+ }
+ result.add(annotatedField);
+ }
+ return result;
+ }
+
+ private static byte[] toInteger(int value) {
+ return toInteger((long) value);
+ }
+
+ private static byte[] toInteger(long value) {
+ return toInteger(BigInteger.valueOf(value));
+ }
+
+ private static byte[] toInteger(BigInteger value) {
+ return createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER,
+ value.toByteArray());
+ }
+
+ private static byte[] toBoolean(boolean value) {
+ // A boolean should be encoded in a single byte with a value of 0 for false and any non-zero
+ // value for true.
+ byte[] result = new byte[1];
+ if (value == false) {
+ result[0] = 0;
+ } else {
+ result[0] = 1;
+ }
+ return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_BOOLEAN, result);
+ }
+
+ private static byte[] toOid(String oid) throws Asn1EncodingException {
+ ByteArrayOutputStream encodedValue = new ByteArrayOutputStream();
+ String[] nodes = oid.split("\\.");
+ if (nodes.length < 2) {
+ throw new Asn1EncodingException(
+ "OBJECT IDENTIFIER must contain at least two nodes: " + oid);
+ }
+ int firstNode;
+ try {
+ firstNode = Integer.parseInt(nodes[0]);
+ } catch (NumberFormatException e) {
+ throw new Asn1EncodingException("Node #1 not numeric: " + nodes[0]);
+ }
+ if ((firstNode > 6) || (firstNode < 0)) {
+ throw new Asn1EncodingException("Invalid value for node #1: " + firstNode);
+ }
+
+ int secondNode;
+ try {
+ secondNode = Integer.parseInt(nodes[1]);
+ } catch (NumberFormatException e) {
+ throw new Asn1EncodingException("Node #2 not numeric: " + nodes[1]);
+ }
+ if ((secondNode >= 40) || (secondNode < 0)) {
+ throw new Asn1EncodingException("Invalid value for node #2: " + secondNode);
+ }
+ int firstByte = firstNode * 40 + secondNode;
+ if (firstByte > 0xff) {
+ throw new Asn1EncodingException(
+ "First two nodes out of range: " + firstNode + "." + secondNode);
+ }
+
+ encodedValue.write(firstByte);
+ for (int i = 2; i < nodes.length; i++) {
+ String nodeString = nodes[i];
+ int node;
+ try {
+ node = Integer.parseInt(nodeString);
+ } catch (NumberFormatException e) {
+ throw new Asn1EncodingException("Node #" + (i + 1) + " not numeric: " + nodeString);
+ }
+ if (node < 0) {
+ throw new Asn1EncodingException("Invalid value for node #" + (i + 1) + ": " + node);
+ }
+ if (node <= 0x7f) {
+ encodedValue.write(node);
+ continue;
+ }
+ if (node < 1 << 14) {
+ encodedValue.write(0x80 | (node >> 7));
+ encodedValue.write(node & 0x7f);
+ continue;
+ }
+ if (node < 1 << 21) {
+ encodedValue.write(0x80 | (node >> 14));
+ encodedValue.write(0x80 | ((node >> 7) & 0x7f));
+ encodedValue.write(node & 0x7f);
+ continue;
+ }
+ throw new Asn1EncodingException("Node #" + (i + 1) + " too large: " + node);
+ }
+
+ return createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER,
+ encodedValue.toByteArray());
+ }
+
+ private static Object getMemberFieldValue(Object obj, Field field)
+ throws Asn1EncodingException {
+ try {
+ return field.get(obj);
+ } catch (ReflectiveOperationException e) {
+ throw new Asn1EncodingException(
+ "Failed to read " + obj.getClass().getName() + "." + field.getName(), e);
+ }
+ }
+
+ private static final class AnnotatedField {
+ private final Field mField;
+ private final Object mObject;
+ private final Asn1Field mAnnotation;
+ private final Asn1Type mDataType;
+ private final Asn1Type mElementDataType;
+ private final Asn1TagClass mTagClass;
+ private final int mDerTagClass;
+ private final int mDerTagNumber;
+ private final Asn1Tagging mTagging;
+ private final boolean mOptional;
+
+ public AnnotatedField(Object obj, Field field, Asn1Field annotation)
+ throws Asn1EncodingException {
+ mObject = obj;
+ mField = field;
+ mAnnotation = annotation;
+ mDataType = annotation.type();
+ mElementDataType = annotation.elementType();
+
+ Asn1TagClass tagClass = annotation.cls();
+ if (tagClass == Asn1TagClass.AUTOMATIC) {
+ if (annotation.tagNumber() != -1) {
+ tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
+ } else {
+ tagClass = Asn1TagClass.UNIVERSAL;
+ }
+ }
+ mTagClass = tagClass;
+ mDerTagClass = BerEncoding.getTagClass(mTagClass);
+
+ int tagNumber;
+ if (annotation.tagNumber() != -1) {
+ tagNumber = annotation.tagNumber();
+ } else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
+ tagNumber = -1;
+ } else {
+ tagNumber = BerEncoding.getTagNumber(mDataType);
+ }
+ mDerTagNumber = tagNumber;
+
+ mTagging = annotation.tagging();
+ if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
+ && (annotation.tagNumber() == -1)) {
+ throw new Asn1EncodingException(
+ "Tag number must be specified when tagging mode is " + mTagging);
+ }
+
+ mOptional = annotation.optional();
+ }
+
+ public Field getField() {
+ return mField;
+ }
+
+ public Asn1Field getAnnotation() {
+ return mAnnotation;
+ }
+
+ public byte[] toDer() throws Asn1EncodingException {
+ Object fieldValue = getMemberFieldValue(mObject, mField);
+ if (fieldValue == null) {
+ if (mOptional) {
+ return null;
+ }
+ throw new Asn1EncodingException("Required field not set");
+ }
+
+ byte[] encoded = JavaToDerConverter.toDer(fieldValue, mDataType, mElementDataType);
+ switch (mTagging) {
+ case NORMAL:
+ return encoded;
+ case EXPLICIT:
+ return createTag(mDerTagClass, true, mDerTagNumber, encoded);
+ case IMPLICIT:
+ int originalTagNumber = BerEncoding.getTagNumber(encoded[0]);
+ if (originalTagNumber == 0x1f) {
+ throw new Asn1EncodingException("High-tag-number form not supported");
+ }
+ if (mDerTagNumber >= 0x1f) {
+ throw new Asn1EncodingException(
+ "Unsupported high tag number: " + mDerTagNumber);
+ }
+ encoded[0] = BerEncoding.setTagNumber(encoded[0], mDerTagNumber);
+ encoded[0] = BerEncoding.setTagClass(encoded[0], mDerTagClass);
+ return encoded;
+ default:
+ throw new RuntimeException("Unknown tagging mode: " + mTagging);
+ }
+ }
+ }
+
+ private static byte[] createTag(
+ int tagClass, boolean constructed, int tagNumber, byte[]... contents) {
+ if (tagNumber >= 0x1f) {
+ throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber);
+ }
+ // tag class & number fit into the first byte
+ byte firstIdentifierByte =
+ (byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber);
+
+ int contentsLength = 0;
+ for (byte[] c : contents) {
+ contentsLength += c.length;
+ }
+ int contentsPosInResult;
+ byte[] result;
+ if (contentsLength < 0x80) {
+ // Length fits into one byte
+ contentsPosInResult = 2;
+ result = new byte[contentsPosInResult + contentsLength];
+ result[0] = firstIdentifierByte;
+ result[1] = (byte) contentsLength;
+ } else {
+ // Length is represented as multiple bytes
+ // The low 7 bits of the first byte represent the number of length bytes (following the
+ // first byte) in which the length is in big-endian base-256 form
+ if (contentsLength <= 0xff) {
+ contentsPosInResult = 3;
+ result = new byte[contentsPosInResult + contentsLength];
+ result[1] = (byte) 0x81; // 1 length byte
+ result[2] = (byte) contentsLength;
+ } else if (contentsLength <= 0xffff) {
+ contentsPosInResult = 4;
+ result = new byte[contentsPosInResult + contentsLength];
+ result[1] = (byte) 0x82; // 2 length bytes
+ result[2] = (byte) (contentsLength >> 8);
+ result[3] = (byte) (contentsLength & 0xff);
+ } else if (contentsLength <= 0xffffff) {
+ contentsPosInResult = 5;
+ result = new byte[contentsPosInResult + contentsLength];
+ result[1] = (byte) 0x83; // 3 length bytes
+ result[2] = (byte) (contentsLength >> 16);
+ result[3] = (byte) ((contentsLength >> 8) & 0xff);
+ result[4] = (byte) (contentsLength & 0xff);
+ } else {
+ contentsPosInResult = 6;
+ result = new byte[contentsPosInResult + contentsLength];
+ result[1] = (byte) 0x84; // 4 length bytes
+ result[2] = (byte) (contentsLength >> 24);
+ result[3] = (byte) ((contentsLength >> 16) & 0xff);
+ result[4] = (byte) ((contentsLength >> 8) & 0xff);
+ result[5] = (byte) (contentsLength & 0xff);
+ }
+ result[0] = firstIdentifierByte;
+ }
+ for (byte[] c : contents) {
+ System.arraycopy(c, 0, result, contentsPosInResult, c.length);
+ contentsPosInResult += c.length;
+ }
+ return result;
+ }
+
+ private static final class JavaToDerConverter {
+ private JavaToDerConverter() {}
+
+ public static byte[] toDer(Object source, Asn1Type targetType, Asn1Type targetElementType)
+ throws Asn1EncodingException {
+ Class<?> sourceType = source.getClass();
+ if (Asn1OpaqueObject.class.equals(sourceType)) {
+ ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded();
+ byte[] result = new byte[buf.remaining()];
+ buf.get(result);
+ return result;
+ }
+
+ if ((targetType == null) || (targetType == Asn1Type.ANY)) {
+ return encode(source);
+ }
+
+ switch (targetType) {
+ case OCTET_STRING:
+ case BIT_STRING:
+ byte[] value = null;
+ if (source instanceof ByteBuffer) {
+ ByteBuffer buf = (ByteBuffer) source;
+ value = new byte[buf.remaining()];
+ buf.slice().get(value);
+ } else if (source instanceof byte[]) {
+ value = (byte[]) source;
+ }
+ if (value != null) {
+ return createTag(
+ BerEncoding.TAG_CLASS_UNIVERSAL,
+ false,
+ BerEncoding.getTagNumber(targetType),
+ value);
+ }
+ break;
+ case INTEGER:
+ if (source instanceof Integer) {
+ return toInteger((Integer) source);
+ } else if (source instanceof Long) {
+ return toInteger((Long) source);
+ } else if (source instanceof BigInteger) {
+ return toInteger((BigInteger) source);
+ }
+ break;
+ case BOOLEAN:
+ if (source instanceof Boolean) {
+ return toBoolean((Boolean) (source));
+ }
+ break;
+ case UTC_TIME:
+ case GENERALIZED_TIME:
+ if (source instanceof String) {
+ return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false,
+ BerEncoding.getTagNumber(targetType), ((String) source).getBytes());
+ }
+ break;
+ case OBJECT_IDENTIFIER:
+ if (source instanceof String) {
+ return toOid((String) source);
+ }
+ break;
+ case SEQUENCE:
+ {
+ Asn1Class containerAnnotation =
+ sourceType.getDeclaredAnnotation(Asn1Class.class);
+ if ((containerAnnotation != null)
+ && (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
+ return toSequence(source);
+ }
+ break;
+ }
+ case CHOICE:
+ {
+ Asn1Class containerAnnotation =
+ sourceType.getDeclaredAnnotation(Asn1Class.class);
+ if ((containerAnnotation != null)
+ && (containerAnnotation.type() == Asn1Type.CHOICE)) {
+ return toChoice(source);
+ }
+ break;
+ }
+ case SET_OF:
+ return toSetOf((Collection<?>) source, targetElementType);
+ case SEQUENCE_OF:
+ return toSequenceOf((Collection<?>) source, targetElementType);
+ default:
+ break;
+ }
+
+ throw new Asn1EncodingException(
+ "Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType);
+ }
+ }
+ /** ASN.1 DER-encoded {@code NULL}. */
+ public static final Asn1OpaqueObject ASN1_DER_NULL =
+ new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java
new file mode 100644
index 0000000000..0002c25cba
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java
new file mode 100644
index 0000000000..d2d3ce049e
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java
new file mode 100644
index 0000000000..672d0e74c6
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java
new file mode 100644
index 0000000000..6cdfcf014c
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java
new file mode 100644
index 0000000000..35fa3744e1
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java
new file mode 100644
index 0000000000..73006222b2
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java
new file mode 100644
index 0000000000..f5604ffdfb
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java
new file mode 100644
index 0000000000..11ef6c3672
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java
new file mode 100644
index 0000000000..8da0a428be
--- /dev/null
+++ b/platform/android/java/editor/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.
+ *
+ * <p>BER data value reader returns data values, one by one, from a source. The interpretation of
+ * data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract
+ * the elements of a SEQUENCE value) is left to clients of the reader.
+ */
+public interface BerDataValueReader {
+
+ /**
+ * Returns the next data value or {@code null} if end of input has been reached.
+ *
+ * @throws BerDataValueFormatException if the value being read is malformed.
+ */
+ BerDataValue readDataValue() throws BerDataValueFormatException;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
new file mode 100644
index 0000000000..d32330c0ad
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java
new file mode 100644
index 0000000000..3fd5291f06
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java
new file mode 100644
index 0000000000..5fbca51db3
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestParser.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestParser.java
new file mode 100644
index 0000000000..ab0a5dad8a
--- /dev/null
+++ b/platform/android/java/editor/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.
+ *
+ * <p>These files consist of a main section followed by individual sections. Individual sections
+ * are named, their names referring to JAR entries.
+ *
+ * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
+ */
+public class ManifestParser {
+
+ private final byte[] mManifest;
+ private int mOffset;
+ private int mEndOffset;
+
+ private byte[] mBufferedLine;
+
+ /**
+ * Constructs a new {@code ManifestParser} with the provided input.
+ */
+ public ManifestParser(byte[] data) {
+ this(data, 0, data.length);
+ }
+
+ /**
+ * Constructs a new {@code ManifestParser} with the provided input.
+ */
+ public ManifestParser(byte[] data, int offset, int length) {
+ mManifest = data;
+ mOffset = offset;
+ mEndOffset = offset + length;
+ }
+
+ /**
+ * Returns the remaining sections of this file.
+ */
+ public List<Section> readAllSections() {
+ List<Section> sections = new ArrayList<>();
+ Section section;
+ while ((section = readSection()) != null) {
+ sections.add(section);
+ }
+ return sections;
+ }
+
+ /**
+ * Returns the next section from this file or {@code null} if end of file has been reached.
+ */
+ public Section readSection() {
+ // Locate the first non-empty line
+ int sectionStartOffset;
+ String attr;
+ do {
+ sectionStartOffset = mOffset;
+ attr = readAttribute();
+ if (attr == null) {
+ return null;
+ }
+ } while (attr.length() == 0);
+ List<Attribute> attrs = new ArrayList<>();
+ attrs.add(parseAttr(attr));
+
+ // Read attributes until end of section reached
+ while (true) {
+ attr = readAttribute();
+ if ((attr == null) || (attr.length() == 0)) {
+ // End of section
+ break;
+ }
+ attrs.add(parseAttr(attr));
+ }
+
+ int sectionEndOffset = mOffset;
+ int sectionSizeBytes = sectionEndOffset - sectionStartOffset;
+
+ return new Section(sectionStartOffset, sectionSizeBytes, attrs);
+ }
+
+ private static Attribute parseAttr(String attr) {
+ // Name is separated from value by a semicolon followed by a single SPACE character.
+ // This permits trailing spaces in names and leading and trailing spaces in values.
+ // Some APK obfuscators take advantage of this fact. We thus need to preserve these unusual
+ // spaces to be able to parse such obfuscated APKs.
+ int delimiterIndex = attr.indexOf(": ");
+ if (delimiterIndex == -1) {
+ return new Attribute(attr, "");
+ } else {
+ return new Attribute(
+ attr.substring(0, delimiterIndex),
+ attr.substring(delimiterIndex + ": ".length()));
+ }
+ }
+
+ /**
+ * Returns the next attribute or empty {@code String} if end of section has been reached or
+ * {@code null} if end of input has been reached.
+ */
+ private String readAttribute() {
+ byte[] bytes = readAttributeBytes();
+ if (bytes == null) {
+ return null;
+ } else if (bytes.length == 0) {
+ return "";
+ } else {
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+ }
+
+ /**
+ * Returns the next attribute or empty array if end of section has been reached or {@code null}
+ * if end of input has been reached.
+ */
+ private byte[] readAttributeBytes() {
+ // Check whether end of section was reached during previous invocation
+ if ((mBufferedLine != null) && (mBufferedLine.length == 0)) {
+ mBufferedLine = null;
+ return EMPTY_BYTE_ARRAY;
+ }
+
+ // Read the next line
+ byte[] line = readLine();
+ if (line == null) {
+ // End of input
+ if (mBufferedLine != null) {
+ byte[] result = mBufferedLine;
+ mBufferedLine = null;
+ return result;
+ }
+ return null;
+ }
+
+ // Consume the read line
+ if (line.length == 0) {
+ // End of section
+ if (mBufferedLine != null) {
+ byte[] result = mBufferedLine;
+ mBufferedLine = EMPTY_BYTE_ARRAY;
+ return result;
+ }
+ return EMPTY_BYTE_ARRAY;
+ }
+ byte[] attrLine;
+ if (mBufferedLine == null) {
+ attrLine = line;
+ } else {
+ if ((line.length == 0) || (line[0] != ' ')) {
+ // The most common case: buffered line is a full attribute
+ byte[] result = mBufferedLine;
+ mBufferedLine = line;
+ return result;
+ }
+ attrLine = mBufferedLine;
+ mBufferedLine = null;
+ attrLine = concat(attrLine, line, 1, line.length - 1);
+ }
+
+ // Everything's buffered in attrLine now. mBufferedLine is null
+
+ // Read more lines
+ while (true) {
+ line = readLine();
+ if (line == null) {
+ // End of input
+ return attrLine;
+ } else if (line.length == 0) {
+ // End of section
+ mBufferedLine = EMPTY_BYTE_ARRAY; // return "end of section" next time
+ return attrLine;
+ }
+ if (line[0] == ' ') {
+ // Continuation line
+ attrLine = concat(attrLine, line, 1, line.length - 1);
+ } else {
+ // Next attribute
+ mBufferedLine = line;
+ return attrLine;
+ }
+ }
+ }
+
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ private static byte[] concat(byte[] arr1, byte[] arr2, int offset2, int length2) {
+ byte[] result = new byte[arr1.length + length2];
+ System.arraycopy(arr1, 0, result, 0, arr1.length);
+ System.arraycopy(arr2, offset2, result, arr1.length, length2);
+ return result;
+ }
+
+ /**
+ * Returns the next line (without line delimiter characters) or {@code null} if end of input has
+ * been reached.
+ */
+ private byte[] readLine() {
+ if (mOffset >= mEndOffset) {
+ return null;
+ }
+ int startOffset = mOffset;
+ int newlineStartOffset = -1;
+ int newlineEndOffset = -1;
+ for (int i = startOffset; i < mEndOffset; i++) {
+ byte b = mManifest[i];
+ if (b == '\r') {
+ newlineStartOffset = i;
+ int nextIndex = i + 1;
+ if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) {
+ newlineEndOffset = nextIndex + 1;
+ break;
+ }
+ newlineEndOffset = nextIndex;
+ break;
+ } else if (b == '\n') {
+ newlineStartOffset = i;
+ newlineEndOffset = i + 1;
+ break;
+ }
+ }
+ if (newlineStartOffset == -1) {
+ newlineStartOffset = mEndOffset;
+ newlineEndOffset = mEndOffset;
+ }
+ mOffset = newlineEndOffset;
+
+ if (newlineStartOffset == startOffset) {
+ return EMPTY_BYTE_ARRAY;
+ }
+ return Arrays.copyOfRange(mManifest, startOffset, newlineStartOffset);
+ }
+
+
+ /**
+ * Attribute.
+ */
+ public static class Attribute {
+ private final String mName;
+ private final String mValue;
+
+ /**
+ * Constructs a new {@code Attribute} with the provided name and value.
+ */
+ public Attribute(String name, String value) {
+ mName = name;
+ mValue = value;
+ }
+
+ /**
+ * Returns this attribute's name.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns this attribute's value.
+ */
+ public String getValue() {
+ return mValue;
+ }
+ }
+
+ /**
+ * Section.
+ */
+ public static class Section {
+ private final int mStartOffset;
+ private final int mSizeBytes;
+ private final String mName;
+ private final List<Attribute> mAttributes;
+
+ /**
+ * Constructs a new {@code Section}.
+ *
+ * @param startOffset start offset (in bytes) of the section in the input file
+ * @param sizeBytes size (in bytes) of the section in the input file
+ * @param attrs attributes contained in the section
+ */
+ public Section(int startOffset, int sizeBytes, List<Attribute> attrs) {
+ mStartOffset = startOffset;
+ mSizeBytes = sizeBytes;
+ String sectionName = null;
+ if (!attrs.isEmpty()) {
+ Attribute firstAttr = attrs.get(0);
+ if ("Name".equalsIgnoreCase(firstAttr.getName())) {
+ sectionName = firstAttr.getValue();
+ }
+ }
+ mName = sectionName;
+ mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs));
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the offset (in bytes) at which this section starts in the input.
+ */
+ public int getStartOffset() {
+ return mStartOffset;
+ }
+
+ /**
+ * Returns the size (in bytes) of this section in the input.
+ */
+ public int getSizeBytes() {
+ return mSizeBytes;
+ }
+
+ /**
+ * Returns this section's attributes, in the order in which they appear in the input.
+ */
+ public List<Attribute> getAttributes() {
+ return mAttributes;
+ }
+
+ /**
+ * Returns the value of the specified attribute in this section or {@code null} if this
+ * section does not contain a matching attribute.
+ */
+ public String getAttributeValue(Attributes.Name name) {
+ return getAttributeValue(name.toString());
+ }
+
+ /**
+ * Returns the value of the specified attribute in this section or {@code null} if this
+ * section does not contain a matching attribute.
+ *
+ * @param name name of the attribute. Attribute names are case-insensitive.
+ */
+ public String getAttributeValue(String name) {
+ for (Attribute attr : mAttributes) {
+ if (attr.getName().equalsIgnoreCase(name)) {
+ return attr.getValue();
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java
new file mode 100644
index 0000000000..fa01beb7b7
--- /dev/null
+++ b/platform/android/java/editor/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 <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
+ */
+public abstract class ManifestWriter {
+
+ private static final byte[] CRLF = new byte[] {'\r', '\n'};
+ private static final int MAX_LINE_LENGTH = 70;
+
+ private ManifestWriter() {}
+
+ public static void writeMainSection(OutputStream out, Attributes attributes)
+ throws IOException {
+
+ // Main section must start with the Manifest-Version attribute.
+ // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
+ String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION);
+ if (manifestVersion == null) {
+ throw new IllegalArgumentException(
+ "Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing");
+ }
+ writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion);
+
+ if (attributes.size() > 1) {
+ SortedMap<String, String> namedAttributes = getAttributesSortedByName(attributes);
+ namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString());
+ writeAttributes(out, namedAttributes);
+ }
+ writeSectionDelimiter(out);
+ }
+
+ public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
+ throws IOException {
+ writeAttribute(out, "Name", name);
+
+ if (!attributes.isEmpty()) {
+ writeAttributes(out, getAttributesSortedByName(attributes));
+ }
+ writeSectionDelimiter(out);
+ }
+
+ static void writeSectionDelimiter(OutputStream out) throws IOException {
+ out.write(CRLF);
+ }
+
+ static void writeAttribute(OutputStream out, Attributes.Name name, String value)
+ throws IOException {
+ writeAttribute(out, name.toString(), value);
+ }
+
+ private static void writeAttribute(OutputStream out, String name, String value)
+ throws IOException {
+ writeLine(out, name + ": " + value);
+ }
+
+ private static void writeLine(OutputStream out, String line) throws IOException {
+ byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8);
+ int offset = 0;
+ int remaining = lineBytes.length;
+ boolean firstLine = true;
+ while (remaining > 0) {
+ int chunkLength;
+ if (firstLine) {
+ // First line
+ chunkLength = Math.min(remaining, MAX_LINE_LENGTH);
+ } else {
+ // Continuation line
+ out.write(CRLF);
+ out.write(' ');
+ chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1);
+ }
+ out.write(lineBytes, offset, chunkLength);
+ offset += chunkLength;
+ remaining -= chunkLength;
+ firstLine = false;
+ }
+ out.write(CRLF);
+ }
+
+ static SortedMap<String, String> getAttributesSortedByName(Attributes attributes) {
+ Set<Map.Entry<Object, Object>> attributesEntries = attributes.entrySet();
+ SortedMap<String, String> namedAttributes = new TreeMap<String, String>();
+ for (Map.Entry<Object, Object> attribute : attributesEntries) {
+ String attrName = attribute.getKey().toString();
+ String attrValue = attribute.getValue().toString();
+ namedAttributes.put(attrName, attrValue);
+ }
+ return namedAttributes;
+ }
+
+ static void writeAttributes(
+ OutputStream out, SortedMap<String, String> attributesSortedByName) throws IOException {
+ for (Map.Entry<String, String> attribute : attributesSortedByName.entrySet()) {
+ String attrName = attribute.getKey();
+ String attrValue = attribute.getValue();
+ writeAttribute(out, attrName, attrValue);
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java
new file mode 100644
index 0000000000..fd8cbff8dc
--- /dev/null
+++ b/platform/android/java/editor/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 <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
+ */
+public abstract class SignatureFileWriter {
+ private SignatureFileWriter() {}
+
+ public static void writeMainSection(OutputStream out, Attributes attributes)
+ throws IOException {
+
+ // Main section must start with the Signature-Version attribute.
+ // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
+ String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION);
+ if (signatureVersion == null) {
+ throw new IllegalArgumentException(
+ "Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing");
+ }
+ ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion);
+
+ if (attributes.size() > 1) {
+ SortedMap<String, String> namedAttributes =
+ ManifestWriter.getAttributesSortedByName(attributes);
+ namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString());
+ ManifestWriter.writeAttributes(out, namedAttributes);
+ }
+ writeSectionDelimiter(out);
+ }
+
+ public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
+ throws IOException {
+ ManifestWriter.writeIndividualSection(out, name, attributes);
+ }
+
+ public static void writeSectionDelimiter(OutputStream out) throws IOException {
+ ManifestWriter.writeSectionDelimiter(out);
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/oid/OidConstants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/oid/OidConstants.java
new file mode 100644
index 0000000000..d80cbaa6ee
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/oid/OidConstants.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.oid;
+
+import com.android.apksig.internal.util.InclusiveIntRange;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class OidConstants {
+ public static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
+ public static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
+ public static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
+ public static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
+ public static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
+ public static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
+
+ public static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
+ public static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
+ public static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
+ public static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
+ public static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
+ public static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
+ public static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
+
+ public static final String OID_SIG_DSA = "1.2.840.10040.4.1";
+ public static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
+ public static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
+ public static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
+ public static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
+ public static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
+
+ public static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
+ public static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
+ public static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
+ public static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
+ public static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
+ public static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
+
+ public static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
+ new HashMap<>();
+ static {
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_RSA,
+ InclusiveIntRange.from(0));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_RSA,
+ InclusiveIntRange.from(0));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.from(0));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_RSA,
+ InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_RSA,
+ InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_RSA,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_RSA,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
+ InclusiveIntRange.fromTo(21, 21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
+ InclusiveIntRange.from(21));
+
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_DSA,
+ InclusiveIntRange.from(0));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.from(9));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_DSA,
+ InclusiveIntRange.from(22));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_DSA,
+ InclusiveIntRange.from(22));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.from(21));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
+ InclusiveIntRange.from(18));
+
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.from(18));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.from(21));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
+ InclusiveIntRange.fromTo(21, 23));
+ addSupportedSigAlg(
+ OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
+ InclusiveIntRange.from(21));
+ }
+
+ public static void addSupportedSigAlg(
+ String digestAlgorithmOid,
+ String signatureAlgorithmOid,
+ InclusiveIntRange... supportedApiLevels) {
+ SUPPORTED_SIG_ALG_OIDS.put(
+ digestAlgorithmOid + "with" + signatureAlgorithmOid,
+ Arrays.asList(supportedApiLevels));
+ }
+
+ public static List<InclusiveIntRange> getSigAlgSupportedApiLevels(
+ String digestAlgorithmOid,
+ String signatureAlgorithmOid) {
+ List<InclusiveIntRange> result =
+ SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
+ return (result != null) ? result : Collections.emptyList();
+ }
+
+ public static class OidToUserFriendlyNameMapper {
+ private OidToUserFriendlyNameMapper() {}
+
+ private static final Map<String, String> OID_TO_USER_FRIENDLY_NAME = new HashMap<>();
+ static {
+ OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA224, "SHA-224");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA256, "SHA-256");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA384, "SHA-384");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA512, "SHA-512");
+
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_RSA, "RSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_MD5_WITH_RSA, "MD5 with RSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_RSA, "SHA-1 with RSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_RSA, "SHA-224 with RSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_RSA, "SHA-256 with RSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_RSA, "SHA-384 with RSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_RSA, "SHA-512 with RSA");
+
+
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_DSA, "DSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_DSA, "SHA-384 with DSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 with DSA");
+
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_ECDSA, "SHA-224 with ECDSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_ECDSA, "SHA-256 with ECDSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_ECDSA, "SHA-384 with ECDSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA");
+ }
+
+ public static String getUserFriendlyNameForOid(String oid) {
+ return OID_TO_USER_FRIENDLY_NAME.get(oid);
+ }
+ }
+
+ public static final Map<String, String> OID_TO_JCA_DIGEST_ALG = new HashMap<>();
+ static {
+ OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5");
+ OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1");
+ OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224");
+ OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256");
+ OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384");
+ OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512");
+ }
+
+ public static final Map<String, String> OID_TO_JCA_SIGNATURE_ALG = new HashMap<>();
+ static {
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA");
+
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA");
+
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA");
+ OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA");
+ }
+
+ private OidConstants() {}
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
new file mode 100644
index 0000000000..9712767293
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.pkcs7;
+
+import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
+import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
+import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1;
+import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_DSA;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA;
+import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA;
+import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_DIGEST_ALG;
+import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_SIGNATURE_ALG;
+
+import com.android.apksig.internal.apk.v1.DigestAlgorithm;
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
+import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.util.Pair;
+
+import java.security.InvalidKeyException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+
+/**
+ * PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class AlgorithmIdentifier {
+
+ @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
+ public String algorithm;
+
+ @Asn1Field(index = 1, type = Asn1Type.ANY, optional = true)
+ public Asn1OpaqueObject parameters;
+
+ public AlgorithmIdentifier() {}
+
+ public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) {
+ this.algorithm = algorithmOid;
+ this.parameters = parameters;
+ }
+
+ /**
+ * Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
+ * algorithm.
+ */
+ public static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
+ DigestAlgorithm digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case SHA1:
+ return new AlgorithmIdentifier(OID_DIGEST_SHA1, ASN1_DER_NULL);
+ case SHA256:
+ return new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL);
+ }
+ throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
+ }
+
+ /**
+ * Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
+ * when signing with the specified key and digest algorithm.
+ */
+ public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
+ PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning)
+ throws InvalidKeyException {
+ String keyAlgorithm = publicKey.getAlgorithm();
+ String jcaDigestPrefixForSigAlg;
+ switch (digestAlgorithm) {
+ case SHA1:
+ jcaDigestPrefixForSigAlg = "SHA1";
+ break;
+ case SHA256:
+ jcaDigestPrefixForSigAlg = "SHA256";
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected digest algorithm: " + digestAlgorithm);
+ }
+ if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
+ return Pair.of(
+ jcaDigestPrefixForSigAlg + "withRSA",
+ new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL));
+ } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
+ AlgorithmIdentifier sigAlgId;
+ switch (digestAlgorithm) {
+ case SHA1:
+ sigAlgId =
+ new AlgorithmIdentifier(OID_SIG_DSA, ASN1_DER_NULL);
+ break;
+ case SHA256:
+ // DSA signatures with SHA-256 in SignedData are accepted by Android API Level
+ // 21 and higher. However, there are two ways to specify their SignedData
+ // SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
+ // dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
+ // the former.
+ sigAlgId =
+ new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected digest algorithm: " + digestAlgorithm);
+ }
+ String signingAlgorithmName =
+ jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA");
+ return Pair.of(signingAlgorithmName, sigAlgId);
+ } else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
+ return Pair.of(
+ jcaDigestPrefixForSigAlg + "withECDSA",
+ new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
+ } else {
+ throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
+ }
+ }
+
+ public static String getJcaSignatureAlgorithm(
+ String digestAlgorithmOid,
+ String signatureAlgorithmOid) throws SignatureException {
+ // First check whether the signature algorithm OID alone is sufficient
+ String result = OID_TO_JCA_SIGNATURE_ALG.get(signatureAlgorithmOid);
+ if (result != null) {
+ return result;
+ }
+
+ // Signature algorithm OID alone is insufficient. Need to combine digest algorithm OID
+ // with signature algorithm OID.
+ String suffix;
+ if (OID_SIG_RSA.equals(signatureAlgorithmOid)) {
+ suffix = "RSA";
+ } else if (OID_SIG_DSA.equals(signatureAlgorithmOid)) {
+ suffix = "DSA";
+ } else if (OID_SIG_EC_PUBLIC_KEY.equals(signatureAlgorithmOid)) {
+ suffix = "ECDSA";
+ } else {
+ throw new SignatureException(
+ "Unsupported JCA Signature algorithm"
+ + " . Digest algorithm: " + digestAlgorithmOid
+ + ", signature algorithm: " + signatureAlgorithmOid);
+ }
+ String jcaDigestAlg = getJcaDigestAlgorithm(digestAlgorithmOid);
+ // Canonical name for SHA-1 with ... is SHA1with, rather than SHA1. Same for all other
+ // SHA algorithms.
+ if (jcaDigestAlg.startsWith("SHA-")) {
+ jcaDigestAlg = "SHA" + jcaDigestAlg.substring("SHA-".length());
+ }
+ return jcaDigestAlg + "with" + suffix;
+ }
+
+ public static String getJcaDigestAlgorithm(String oid)
+ throws SignatureException {
+ String result = OID_TO_JCA_DIGEST_ALG.get(oid);
+ if (result == null) {
+ throw new SignatureException("Unsupported digest algorithm: " + oid);
+ }
+ return result;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java
new file mode 100644
index 0000000000..a6c91efac6
--- /dev/null
+++ b/platform/android/java/editor/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<Asn1OpaqueObject> attrValues;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java
new file mode 100644
index 0000000000..8ab722c2db
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java
new file mode 100644
index 0000000000..79f41af89d
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java
new file mode 100644
index 0000000000..284b11764b
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java
new file mode 100644
index 0000000000..1a115d5156
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java
new file mode 100644
index 0000000000..4004ee7f78
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java
new file mode 100644
index 0000000000..56b6e502dc
--- /dev/null
+++ b/platform/android/java/editor/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<AlgorithmIdentifier> digestAlgorithms;
+
+ @Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
+ public EncapsulatedContentInfo encapContentInfo;
+
+ @Asn1Field(
+ index = 3,
+ type = Asn1Type.SET_OF,
+ tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
+ optional = true)
+ public List<Asn1OpaqueObject> certificates;
+
+ @Asn1Field(
+ index = 4,
+ type = Asn1Type.SET_OF,
+ tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
+ optional = true)
+ public List<ByteBuffer> crls;
+
+ @Asn1Field(index = 5, type = Asn1Type.SET_OF)
+ public List<SignerInfo> signerInfos;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java
new file mode 100644
index 0000000000..a3d70f16bb
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java
new file mode 100644
index 0000000000..b885eb8002
--- /dev/null
+++ b/platform/android/java/editor/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<Attribute> unsignedAttrs;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java
new file mode 100644
index 0000000000..90aee30321
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.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.util;
+
+/**
+ * Android SDK version / API Level constants.
+ */
+public abstract class AndroidSdkVersion {
+
+ /** Hidden constructor to prevent instantiation. */
+ private AndroidSdkVersion() {}
+
+ /** Android 1.0 */
+ public static final int INITIAL_RELEASE = 1;
+
+ /** Android 2.3. */
+ public static final int GINGERBREAD = 9;
+
+ /** Android 3.0 */
+ public static final int HONEYCOMB = 11;
+
+ /** Android 4.3. The revenge of the beans. */
+ public static final int JELLY_BEAN_MR2 = 18;
+
+ /** Android 4.4. KitKat, another tasty treat. */
+ public static final int KITKAT = 19;
+
+ /** Android 5.0. A flat one with beautiful shadows. But still tasty. */
+ public static final int LOLLIPOP = 21;
+
+ /** Android 6.0. M is for Marshmallow! */
+ public static final int M = 23;
+
+ /** Android 7.0. N is for Nougat. */
+ public static final int N = 24;
+
+ /** Android O. */
+ public static final int O = 26;
+
+ /** Android P. */
+ public static final int P = 28;
+
+ /** Android Q. */
+ public static final int Q = 29;
+
+ /** Android R. */
+ public static final int R = 30;
+
+ /** Android S. */
+ public static final int S = 31;
+
+ /** Android Sv2. */
+ public static final int Sv2 = 32;
+
+ /** Android Tiramisu. */
+ public static final int T = 33;
+
+ /** Android Upside Down Cake. */
+ public static final int U = 34;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java
new file mode 100644
index 0000000000..e5741a5b53
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java
new file mode 100644
index 0000000000..656c20e111
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java
new file mode 100644
index 0000000000..d7cbe03511
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java
new file mode 100644
index 0000000000..a7b4b5c804
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteStreams.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteStreams.java
new file mode 100644
index 0000000000..bca3b0827b
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java
new file mode 100644
index 0000000000..a0baf1aeff
--- /dev/null
+++ b/platform/android/java/editor/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<Integer, Long> firstSource = locateDataSource(offset);
+ int i = firstSource.getFirst();
+ offset = firstSource.getSecond();
+
+ // Return the current source's ByteBuffer if it fits.
+ if (offset + size <= mSources[i].size()) {
+ return mSources[i].getByteBuffer(offset, size);
+ }
+
+ // Otherwise, read into a new buffer.
+ ByteBuffer buffer = ByteBuffer.allocate(size);
+ for (; i < mSources.length && buffer.hasRemaining(); i++) {
+ long sizeToCopy = Math.min(mSources[i].size() - offset, buffer.remaining());
+ mSources[i].copyTo(offset, Math.toIntExact(sizeToCopy), buffer);
+ offset = 0; // may not be zero for the first source, but reset after that.
+ }
+ buffer.rewind();
+ return buffer;
+ }
+
+ @Override
+ public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
+ feed(offset, size, new ByteBufferSink(dest));
+ }
+
+ @Override
+ public DataSource slice(long offset, long size) {
+ // Find the first slice.
+ Pair<Integer, Long> firstSource = locateDataSource(offset);
+ int beginIndex = firstSource.getFirst();
+ long beginLocalOffset = firstSource.getSecond();
+ DataSource beginSource = mSources[beginIndex];
+
+ if (beginLocalOffset + size <= beginSource.size()) {
+ return beginSource.slice(beginLocalOffset, size);
+ }
+
+ // Add the first slice to chaining, followed by the middle full slices, then the last.
+ ArrayList<DataSource> sources = new ArrayList<>();
+ sources.add(beginSource.slice(
+ beginLocalOffset, beginSource.size() - beginLocalOffset));
+
+ Pair<Integer, Long> lastSource = locateDataSource(offset + size - 1);
+ int endIndex = lastSource.getFirst();
+ long endLocalOffset = lastSource.getSecond();
+
+ for (int i = beginIndex + 1; i < endIndex; i++) {
+ sources.add(mSources[i]);
+ }
+
+ sources.add(mSources[endIndex].slice(0, endLocalOffset + 1));
+ return new ChainedDataSource(sources.toArray(new DataSource[0]));
+ }
+
+ /**
+ * Find the index of DataSource that offset is at.
+ * @return Pair of DataSource index and the local offset in the DataSource.
+ */
+ private Pair<Integer, Long> locateDataSource(long offset) {
+ long localOffset = offset;
+ for (int i = 0; i < mSources.length; i++) {
+ if (localOffset < mSources[i].size()) {
+ return Pair.of(i, localOffset);
+ }
+ localOffset -= mSources[i].size();
+ }
+ throw new IndexOutOfBoundsException("Access is out of bound, offset: " + offset +
+ ", totalSize: " + mTotalSize);
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java
new file mode 100644
index 0000000000..2a890f6868
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.util;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Principal;
+import java.security.Provider;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * {@link X509Certificate} which delegates all method invocations to the provided delegate
+ * {@code X509Certificate}.
+ */
+public class DelegatingX509Certificate extends X509Certificate {
+ private static final long serialVersionUID = 1L;
+
+ private final X509Certificate mDelegate;
+
+ public DelegatingX509Certificate(X509Certificate delegate) {
+ this.mDelegate = delegate;
+ }
+
+ @Override
+ public Set<String> getCriticalExtensionOIDs() {
+ return mDelegate.getCriticalExtensionOIDs();
+ }
+
+ @Override
+ public byte[] getExtensionValue(String oid) {
+ return mDelegate.getExtensionValue(oid);
+ }
+
+ @Override
+ public Set<String> getNonCriticalExtensionOIDs() {
+ return mDelegate.getNonCriticalExtensionOIDs();
+ }
+
+ @Override
+ public boolean hasUnsupportedCriticalExtension() {
+ return mDelegate.hasUnsupportedCriticalExtension();
+ }
+
+ @Override
+ public void checkValidity()
+ throws CertificateExpiredException, CertificateNotYetValidException {
+ mDelegate.checkValidity();
+ }
+
+ @Override
+ public void checkValidity(Date date)
+ throws CertificateExpiredException, CertificateNotYetValidException {
+ mDelegate.checkValidity(date);
+ }
+
+ @Override
+ public int getVersion() {
+ return mDelegate.getVersion();
+ }
+
+ @Override
+ public BigInteger getSerialNumber() {
+ return mDelegate.getSerialNumber();
+ }
+
+ @Override
+ public Principal getIssuerDN() {
+ return mDelegate.getIssuerDN();
+ }
+
+ @Override
+ public Principal getSubjectDN() {
+ return mDelegate.getSubjectDN();
+ }
+
+ @Override
+ public Date getNotBefore() {
+ return mDelegate.getNotBefore();
+ }
+
+ @Override
+ public Date getNotAfter() {
+ return mDelegate.getNotAfter();
+ }
+
+ @Override
+ public byte[] getTBSCertificate() throws CertificateEncodingException {
+ return mDelegate.getTBSCertificate();
+ }
+
+ @Override
+ public byte[] getSignature() {
+ return mDelegate.getSignature();
+ }
+
+ @Override
+ public String getSigAlgName() {
+ return mDelegate.getSigAlgName();
+ }
+
+ @Override
+ public String getSigAlgOID() {
+ return mDelegate.getSigAlgOID();
+ }
+
+ @Override
+ public byte[] getSigAlgParams() {
+ return mDelegate.getSigAlgParams();
+ }
+
+ @Override
+ public boolean[] getIssuerUniqueID() {
+ return mDelegate.getIssuerUniqueID();
+ }
+
+ @Override
+ public boolean[] getSubjectUniqueID() {
+ return mDelegate.getSubjectUniqueID();
+ }
+
+ @Override
+ public boolean[] getKeyUsage() {
+ return mDelegate.getKeyUsage();
+ }
+
+ @Override
+ public int getBasicConstraints() {
+ return mDelegate.getBasicConstraints();
+ }
+
+ @Override
+ public byte[] getEncoded() throws CertificateEncodingException {
+ return mDelegate.getEncoded();
+ }
+
+ @Override
+ public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
+ InvalidKeyException, NoSuchProviderException, SignatureException {
+ mDelegate.verify(key);
+ }
+
+ @Override
+ public void verify(PublicKey key, String sigProvider)
+ throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
+ NoSuchProviderException, SignatureException {
+ mDelegate.verify(key, sigProvider);
+ }
+
+ @Override
+ public String toString() {
+ return mDelegate.toString();
+ }
+
+ @Override
+ public PublicKey getPublicKey() {
+ return mDelegate.getPublicKey();
+ }
+
+ @Override
+ public X500Principal getIssuerX500Principal() {
+ return mDelegate.getIssuerX500Principal();
+ }
+
+ @Override
+ public X500Principal getSubjectX500Principal() {
+ return mDelegate.getSubjectX500Principal();
+ }
+
+ @Override
+ public List<String> getExtendedKeyUsage() throws CertificateParsingException {
+ return mDelegate.getExtendedKeyUsage();
+ }
+
+ @Override
+ public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException {
+ return mDelegate.getSubjectAlternativeNames();
+ }
+
+ @Override
+ public Collection<List<?>> getIssuerAlternativeNames() throws CertificateParsingException {
+ return mDelegate.getIssuerAlternativeNames();
+ }
+
+ @Override
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ public void verify(PublicKey key, Provider sigProvider) throws CertificateException,
+ NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+ mDelegate.verify(key, sigProvider);
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java
new file mode 100644
index 0000000000..e4a421a72c
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.internal.util;
+
+import com.android.apksig.util.DataSink;
+import com.android.apksig.util.DataSource;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * {@link DataSource} backed by a {@link FileChannel} for {@link RandomAccessFile} access.
+ */
+public class FileChannelDataSource implements DataSource {
+
+ private static final int MAX_READ_CHUNK_SIZE = 1024 * 1024;
+
+ private final FileChannel mChannel;
+ private final long mOffset;
+ private final long mSize;
+
+ /**
+ * Constructs a new {@code FileChannelDataSource} based on the data contained in the
+ * whole file. Changes to the contents of the file, including the size of the file,
+ * will be visible in this data source.
+ */
+ public FileChannelDataSource(FileChannel channel) {
+ mChannel = channel;
+ mOffset = 0;
+ mSize = -1;
+ }
+
+ /**
+ * Constructs a new {@code FileChannelDataSource} based on the data contained in the
+ * specified region of the provided file. Changes to the contents of the file will be visible in
+ * this data source.
+ *
+ * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative.
+ */
+ public FileChannelDataSource(FileChannel channel, long offset, long size) {
+ if (offset < 0) {
+ throw new IndexOutOfBoundsException("offset: " + size);
+ }
+ if (size < 0) {
+ throw new IndexOutOfBoundsException("size: " + size);
+ }
+ mChannel = channel;
+ mOffset = offset;
+ mSize = size;
+ }
+
+ @Override
+ public long size() {
+ if (mSize == -1) {
+ try {
+ return mChannel.size();
+ } catch (IOException e) {
+ return 0;
+ }
+ } else {
+ return mSize;
+ }
+ }
+
+ @Override
+ public FileChannelDataSource slice(long offset, long size) {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if ((offset == 0) && (size == sourceSize)) {
+ return this;
+ }
+
+ return new FileChannelDataSource(mChannel, mOffset + offset, size);
+ }
+
+ @Override
+ public void feed(long offset, long size, DataSink sink) throws IOException {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if (size == 0) {
+ return;
+ }
+
+ long chunkOffsetInFile = mOffset + offset;
+ long remaining = size;
+ ByteBuffer buf = ByteBuffer.allocateDirect((int) Math.min(remaining, MAX_READ_CHUNK_SIZE));
+
+ while (remaining > 0) {
+ int chunkSize = (int) Math.min(remaining, buf.capacity());
+ int chunkRemaining = chunkSize;
+ buf.limit(chunkSize);
+ synchronized (mChannel) {
+ mChannel.position(chunkOffsetInFile);
+ while (chunkRemaining > 0) {
+ int read = mChannel.read(buf);
+ if (read < 0) {
+ throw new IOException("Unexpected EOF encountered");
+ }
+ chunkRemaining -= read;
+ }
+ }
+ buf.flip();
+ sink.consume(buf);
+ buf.clear();
+ chunkOffsetInFile += chunkSize;
+ remaining -= chunkSize;
+ }
+ }
+
+ @Override
+ public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
+ long sourceSize = size();
+ checkChunkValid(offset, size, sourceSize);
+ if (size == 0) {
+ return;
+ }
+ if (size > dest.remaining()) {
+ throw new BufferOverflowException();
+ }
+
+ long offsetInFile = mOffset + offset;
+ int remaining = size;
+ int prevLimit = dest.limit();
+ try {
+ // FileChannel.read(ByteBuffer) reads up to dest.remaining(). Thus, we need to adjust
+ // the buffer's limit to avoid reading more than size bytes.
+ dest.limit(dest.position() + size);
+ while (remaining > 0) {
+ int chunkSize;
+ synchronized (mChannel) {
+ mChannel.position(offsetInFile);
+ chunkSize = mChannel.read(dest);
+ }
+ offsetInFile += chunkSize;
+ remaining -= chunkSize;
+ }
+ } finally {
+ dest.limit(prevLimit);
+ }
+ }
+
+ @Override
+ public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
+ if (size < 0) {
+ throw new IndexOutOfBoundsException("size: " + size);
+ }
+ ByteBuffer result = ByteBuffer.allocate(size);
+ copyTo(offset, size, result);
+ result.flip();
+ return result;
+ }
+
+ private static void checkChunkValid(long offset, long size, long sourceSize) {
+ if (offset < 0) {
+ throw new IndexOutOfBoundsException("offset: " + offset);
+ }
+ if (size < 0) {
+ throw new IndexOutOfBoundsException("size: " + size);
+ }
+ if (offset > sourceSize) {
+ throw new IndexOutOfBoundsException(
+ "offset (" + offset + ") > source size (" + sourceSize + ")");
+ }
+ long endOffset = offset + size;
+ if (endOffset < offset) {
+ throw new IndexOutOfBoundsException(
+ "offset (" + offset + ") + size (" + size + ") overflow");
+ }
+ if (endOffset > sourceSize) {
+ throw new IndexOutOfBoundsException(
+ "offset (" + offset + ") + size (" + size
+ + ") > source size (" + sourceSize +")");
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java
new file mode 100644
index 0000000000..958cd12aa3
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java
new file mode 100644
index 0000000000..d7866a9edd
--- /dev/null
+++ b/platform/android/java/editor/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<InclusiveIntRange> getValuesNotIn(
+ List<InclusiveIntRange> sortedNonOverlappingRanges) {
+ if (sortedNonOverlappingRanges.isEmpty()) {
+ return Collections.singletonList(this);
+ }
+
+ int testValue = min;
+ List<InclusiveIntRange> result = null;
+ for (InclusiveIntRange range : sortedNonOverlappingRanges) {
+ int rangeMax = range.max;
+ if (testValue > rangeMax) {
+ continue;
+ }
+ int rangeMin = range.min;
+ if (testValue < range.min) {
+ if (result == null) {
+ result = new ArrayList<>();
+ }
+ result.add(fromTo(testValue, rangeMin - 1));
+ }
+ if (rangeMax >= max) {
+ return (result != null) ? result : Collections.emptyList();
+ }
+ testValue = rangeMax + 1;
+ }
+ if (testValue <= max) {
+ if (result == null) {
+ result = new ArrayList<>(1);
+ }
+ result.add(fromTo(testValue, max));
+ }
+ return (result != null) ? result : Collections.emptyList();
+ }
+
+ @Override
+ public String toString() {
+ return "[" + min + ", " + ((max < Integer.MAX_VALUE) ? (max + "]") : "\u221e)");
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java
new file mode 100644
index 0000000000..733dd563ce
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java
new file mode 100644
index 0000000000..f1b5ac6c5e
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/Pair.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/Pair.java
new file mode 100644
index 0000000000..7f9ee520f1
--- /dev/null
+++ b/platform/android/java/editor/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<A, B> {
+ private final A mFirst;
+ private final B mSecond;
+
+ private Pair(A first, B second) {
+ mFirst = first;
+ mSecond = second;
+ }
+
+ public static <A, B> Pair<A, B> of(A first, B second) {
+ return new Pair<A, B>(first, second);
+ }
+
+ public A getFirst() {
+ return mFirst;
+ }
+
+ public B getSecond() {
+ return mSecond;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
+ result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ @SuppressWarnings("rawtypes")
+ Pair other = (Pair) obj;
+ if (mFirst == null) {
+ if (other.mFirst != null) {
+ return false;
+ }
+ } else if (!mFirst.equals(other.mFirst)) {
+ return false;
+ }
+ if (mSecond == null) {
+ if (other.mSecond != null) {
+ return false;
+ }
+ } else if (!mSecond.equals(other.mSecond)) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java
new file mode 100644
index 0000000000..bbd2d14a8a
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/TeeDataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/TeeDataSink.java
new file mode 100644
index 0000000000..2e46f18b05
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
new file mode 100644
index 0000000000..81026ba5ff
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java
@@ -0,0 +1,325 @@
+/*
+ * 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 static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+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.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import java.util.ArrayList;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 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 implements AutoCloseable {
+
+ /**
+ * Maximum size (in bytes) of each node of the tree.
+ */
+ private final static int CHUNK_SIZE = 4096;
+ /**
+ * Maximum parallelism while calculating digests.
+ */
+ private final static int DIGEST_PARALLELISM = Math.min(32,
+ Runtime.getRuntime().availableProcessors());
+ /**
+ * Queue size.
+ */
+ private final static int MAX_OUTSTANDING_CHUNKS = 4;
+ /**
+ * Typical prefetch size.
+ */
+ private final static int MAX_PREFETCH_CHUNKS = 1024;
+ /**
+ * Minimum chunks to be processed by a single worker task.
+ */
+ private final static int MIN_CHUNKS_PER_WORKER = 8;
+
+ /**
+ * 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;
+
+ private final ExecutorService mExecutor =
+ new ThreadPoolExecutor(DIGEST_PARALLELISM, DIGEST_PARALLELISM,
+ 0L, MILLISECONDS,
+ new ArrayBlockingQueue<>(MAX_OUTSTANDING_CHUNKS),
+ new ThreadPoolExecutor.CallerRunsPolicy());
+
+ public VerityTreeBuilder(byte[] salt) throws NoSuchAlgorithmException {
+ mSalt = salt;
+ mMd = getNewMessageDigest();
+ }
+
+ @Override
+ public void close() {
+ mExecutor.shutdownNow();
+ }
+
+ /**
+ * 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.
+ */
+ public byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
+ ByteBuffer verityBuffer = generateVerityTree(fileSource);
+ return getRootHashFromTree(verityBuffer);
+ }
+
+ /**
+ * Returns the byte buffer that contains the whole verity tree.
+ *
+ * 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.
+ */
+ public ByteBuffer generateVerityTree(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);
+ }
+ }
+ return verityBuffer;
+ }
+
+ /**
+ * Returns the digested root hash from the top level (only page) of a verity tree.
+ */
+ public byte[] getRootHashFromTree(ByteBuffer verityBuffer) throws IOException {
+ 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<Long> 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 {
+ final long size = dataSource.size();
+ final int chunks = (int) divideRoundup(size, CHUNK_SIZE);
+
+ /** Single IO operation size, in chunks. */
+ final int ioSizeChunks = MAX_PREFETCH_CHUNKS;
+
+ final byte[][] hashes = new byte[chunks][];
+
+ Phaser tasks = new Phaser(1);
+
+ // Reading the input file as fast as we can.
+ final long maxReadSize = ioSizeChunks * CHUNK_SIZE;
+
+ long readOffset = 0;
+ int startChunkIndex = 0;
+ while (readOffset < size) {
+ final long readLimit = Math.min(readOffset + maxReadSize, size);
+ final int readSize = (int) (readLimit - readOffset);
+ final int bufferSizeChunks = (int) divideRoundup(readSize, CHUNK_SIZE);
+
+ // Overllocating to zero-pad last chunk.
+ // With 4MiB block size, 32 threads and 4 queue size we might allocate up to 144MiB.
+ final ByteBuffer buffer = ByteBuffer.allocate(bufferSizeChunks * CHUNK_SIZE);
+ dataSource.copyTo(readOffset, readSize, buffer);
+ buffer.rewind();
+
+ final int readChunkIndex = startChunkIndex;
+ Runnable task = () -> {
+ final MessageDigest md = cloneMessageDigest();
+ for (int offset = 0, finish = buffer.capacity(), chunkIndex = readChunkIndex;
+ offset < finish; offset += CHUNK_SIZE, ++chunkIndex) {
+ ByteBuffer chunk = slice(buffer, offset, offset + CHUNK_SIZE);
+ hashes[chunkIndex] = saltedDigest(md, chunk);
+ }
+ tasks.arriveAndDeregister();
+ };
+ tasks.register();
+ mExecutor.execute(task);
+
+ startChunkIndex += bufferSizeChunks;
+ readOffset += readSize;
+ }
+
+ // Waiting for the tasks to complete.
+ tasks.arriveAndAwaitAdvance();
+
+ // Streaming hashes back.
+ for (byte[] hash : hashes) {
+ dataSink.consume(hash, 0, hash.length);
+ }
+ }
+
+ /** Returns the digest of data with salt prepended. */
+ private byte[] saltedDigest(ByteBuffer data) {
+ return saltedDigest(mMd, data);
+ }
+
+ private byte[] saltedDigest(MessageDigest md, ByteBuffer data) {
+ md.reset();
+ if (mSalt != null) {
+ md.update(mSalt);
+ }
+ md.update(data);
+ return md.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();
+ }
+
+ /**
+ * Obtains a new instance of the message digest algorithm.
+ */
+ private static MessageDigest getNewMessageDigest() throws NoSuchAlgorithmException {
+ return MessageDigest.getInstance(JCA_ALGORITHM);
+ }
+
+ /**
+ * Clones the existing message digest, or creates a new instance if clone is unavailable.
+ */
+ private MessageDigest cloneMessageDigest() {
+ try {
+ return (MessageDigest) mMd.clone();
+ } catch (CloneNotSupportedException ignored) {
+ try {
+ return getNewMessageDigest();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(
+ "Failed to obtain an instance of a previously available message digest", e);
+ }
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java
new file mode 100644
index 0000000000..ca6271df2a
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java
@@ -0,0 +1,282 @@
+/*
+ * 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 volatile 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;
+ }
+
+ buildCertFactoryHelper();
+ }
+
+ private static synchronized void buildCertFactoryHelper() {
+ 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 {
+ 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<? extends java.security.cert.Certificate> generateCertificates(
+ InputStream in) throws CertificateException {
+ 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<? extends java.security.cert.Certificate> 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<X509Certificate> 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/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java
new file mode 100644
index 0000000000..077db232ba
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Certificate.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Certificate.java
new file mode 100644
index 0000000000..70ff6a163c
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Certificate.java
@@ -0,0 +1,105 @@
+/*
+ * 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;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
+import com.android.apksig.internal.pkcs7.SignerIdentifier;
+import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * 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;
+
+ public static X509Certificate findCertificate(
+ Collection<X509Certificate> certs, SignerIdentifier id) {
+ for (X509Certificate cert : certs) {
+ if (isMatchingCerticicate(cert, id)) {
+ return cert;
+ }
+ }
+ return null;
+ }
+
+ 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());
+ }
+
+ public static List<X509Certificate> parseCertificates(
+ List<Asn1OpaqueObject> encodedCertificates) throws CertificateException {
+ if (encodedCertificates.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<X509Certificate> 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;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Extension.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Extension.java
new file mode 100644
index 0000000000..bf37c1e824
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Name.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Name.java
new file mode 100644
index 0000000000..08400d6814
--- /dev/null
+++ b/platform/android/java/editor/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<RelativeDistinguishedName> relativeDistinguishedNames;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RSAPublicKey.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RSAPublicKey.java
new file mode 100644
index 0000000000..521e067c26
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RSAPublicKey.java
@@ -0,0 +1,35 @@
+/*
+ * 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.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.math.BigInteger;
+
+/**
+ * {@code RSAPublicKey} as specified in RFC 3279.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class RSAPublicKey {
+ @Asn1Field(index = 0, type = Asn1Type.INTEGER)
+ public BigInteger modulus;
+
+ @Asn1Field(index = 1, type = Asn1Type.INTEGER)
+ public BigInteger publicExponent;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java
new file mode 100644
index 0000000000..bb89e8d33a
--- /dev/null
+++ b/platform/android/java/editor/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<AttributeTypeAndValue> attributes;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java
new file mode 100644
index 0000000000..821523761b
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java
new file mode 100644
index 0000000000..922f52c205
--- /dev/null
+++ b/platform/android/java/editor/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<Extension> extensions;
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Time.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Time.java
new file mode 100644
index 0000000000..def2ee8947
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Validity.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Validity.java
new file mode 100644
index 0000000000..df9acb3f36
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java
new file mode 100644
index 0000000000..d2f444ddcd
--- /dev/null
+++ b/platform/android/java/editor/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<CentralDirectoryRecord> 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<CentralDirectoryRecord> {
+ @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/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/EocdRecord.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/EocdRecord.java
new file mode 100644
index 0000000000..d2000b42dd
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/EocdRecord.java
@@ -0,0 +1,57 @@
+/*
+ * 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;
+ }
+
+ public static ByteBuffer createWithPaddedComment(ByteBuffer original, int padding) {
+ ByteBuffer result = ByteBuffer.allocate((int) original.remaining() + padding);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.put(original.slice());
+ result.rewind();
+ ZipUtils.updateZipEocdCommentLen(result);
+ return result;
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java
new file mode 100644
index 0000000000..50ce386aa7
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java
@@ -0,0 +1,543 @@
+/*
+ * 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.
+ *
+ * <p>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 = null;
+ try {
+ result = new byte[(int) cdRecord.getUncompressedSize()];
+ } catch (OutOfMemoryError e) {
+ throw new IOException(
+ cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize(), e);
+ }
+ 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/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/ZipUtils.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/ZipUtils.java
new file mode 100644
index 0000000000..1c2e82cdab
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/ZipUtils.java
@@ -0,0 +1,385 @@
+/*
+ * 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.apk.ApkFormatException;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.util.DataSource;
+import com.android.apksig.zip.ZipFormatException;
+import com.android.apksig.zip.ZipSections;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+
+/**
+ * Assorted ZIP format helpers.
+ *
+ * <p>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.
+ *
+ * <p>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);
+ }
+
+ /**
+ * Sets the length of EOCD comment.
+ *
+ * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+ */
+ public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
+ assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+ int commentLen = zipEndOfCentralDirectory.remaining() - ZIP_EOCD_REC_MIN_SIZE;
+ setUnsignedInt16(
+ zipEndOfCentralDirectory,
+ zipEndOfCentralDirectory.position() + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET,
+ commentLen);
+ }
+
+ /**
+ * Returns the offset of the start of the ZIP Central Directory in the archive.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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<ByteBuffer, Long> 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<ByteBuffer, Long> 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<ByteBuffer, Long> 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.
+ *
+ * <p>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;
+ }
+
+ public static List<CentralDirectoryRecord> parseZipCentralDirectory(
+ DataSource apk,
+ 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<CentralDirectoryRecord> 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;
+ }
+
+ 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/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSink.java
new file mode 100644
index 0000000000..5042933f1f
--- /dev/null
+++ b/platform/android/java/editor/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.
+ *
+ * <p>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.
+ *
+ * <p>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/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSinks.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSinks.java
new file mode 100644
index 0000000000..d9562d8341
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSource.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSource.java
new file mode 100644
index 0000000000..a89a87c5f8
--- /dev/null
+++ b/platform/android/java/editor/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.
+ *
+ * <p>This abstraction serves three purposes:
+ * <ul>
+ * <li>Transparent handling of different types of sources, such as {@code byte[]},
+ * {@link java.nio.ByteBuffer}, {@link java.io.RandomAccessFile}, memory-mapped file.</li>
+ * <li>Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer}
+ * may have worked as the unifying abstraction.</li>
+ * <li>Support sources which do not fit into logical memory as a contiguous region.</li>
+ * </ul>
+ *
+ * <p>There are following ways to obtain a chunk of data from the data source:
+ * <ul>
+ * <li>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.</li>
+ * <li>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.</li>
+ * <li>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.
+ * </li>
+ * </ul>
+ */
+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.
+ *
+ * <p>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/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSources.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSources.java
new file mode 100644
index 0000000000..1f0b40b66a
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSources.java
@@ -0,0 +1,80 @@
+/*
+ * 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.FileChannelDataSource;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * 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) {
+ return asDataSource(file.getChannel());
+ }
+
+ /**
+ * 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) {
+ return asDataSource(file.getChannel(), offset, size);
+ }
+
+ /**
+ * Returns a {@link DataSource} backed by the provided {@link FileChannel}. Changes to the
+ * file, including changes to size of file, will be visible in the data source.
+ */
+ public static DataSource asDataSource(FileChannel channel) {
+ if (channel == null) {
+ throw new NullPointerException();
+ }
+ return new FileChannelDataSource(channel);
+ }
+
+ /**
+ * Returns a {@link DataSource} backed by the provided region of the {@link FileChannel}.
+ * Changes to the file will be visible in the data source.
+ */
+ public static DataSource asDataSource(FileChannel channel, long offset, long size) {
+ if (channel == null) {
+ throw new NullPointerException();
+ }
+ return new FileChannelDataSource(channel, offset, size);
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/util/ReadableDataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/ReadableDataSink.java
new file mode 100644
index 0000000000..ffc3e2d351
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesExecutor.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesExecutor.java
new file mode 100644
index 0000000000..74017f8d8c
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesExecutor.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.ThreadPoolExecutor;
+
+public interface RunnablesExecutor {
+ static final RunnablesExecutor SINGLE_THREADED = p -> p.createRunnable().run();
+
+ static final RunnablesExecutor MULTI_THREADED = new RunnablesExecutor() {
+ private final int PARALLELISM = Math.min(32, Runtime.getRuntime().availableProcessors());
+ private final int QUEUE_SIZE = 4;
+
+ @Override
+ public void execute(RunnablesProvider provider) {
+ final ExecutorService mExecutor =
+ new ThreadPoolExecutor(PARALLELISM, PARALLELISM,
+ 0L, MILLISECONDS,
+ new ArrayBlockingQueue<>(QUEUE_SIZE),
+ new ThreadPoolExecutor.CallerRunsPolicy());
+
+ Phaser tasks = new Phaser(1);
+
+ for (int i = 0; i < PARALLELISM; ++i) {
+ Runnable task = () -> {
+ Runnable r = provider.createRunnable();
+ r.run();
+ tasks.arriveAndDeregister();
+ };
+ tasks.register();
+ mExecutor.execute(task);
+ }
+
+ // Waiting for the tasks to complete.
+ tasks.arriveAndAwaitAdvance();
+
+ mExecutor.shutdownNow();
+ }
+ };
+
+ void execute(RunnablesProvider provider);
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesProvider.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesProvider.java
new file mode 100644
index 0000000000..f96dcfe4d1
--- /dev/null
+++ b/platform/android/java/editor/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 createRunnable();
+}
diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipFormatException.java b/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipFormatException.java
new file mode 100644
index 0000000000..6116c0da80
--- /dev/null
+++ b/platform/android/java/editor/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/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipSections.java b/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipSections.java
new file mode 100644
index 0000000000..17bce05187
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipSections.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksig.zip;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Base representation of an APK's zip sections containing the central directory's offset, the size
+ * of the central directory in bytes, the number of records in the central directory, the offset
+ * of the end of central directory, and a ByteBuffer containing the end of central directory
+ * contents.
+ */
+public 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;
+ }
+} \ No newline at end of file
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
index 5515347bd6..9cc133046b 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
@@ -43,8 +43,11 @@ import android.widget.Toast
import androidx.annotation.CallSuper
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.window.layout.WindowMetricsCalculator
+import org.godotengine.editor.utils.signApk
+import org.godotengine.editor.utils.verifyApk
import org.godotengine.godot.GodotActivity
import org.godotengine.godot.GodotLib
+import org.godotengine.godot.error.Error
import org.godotengine.godot.utils.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix
import java.util.*
@@ -203,7 +206,14 @@ open class GodotEditor : GodotActivity() {
}
if (editorWindowInfo.windowClassName == javaClass.name) {
Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
- ProcessPhoenix.triggerRebirth(this, newInstance)
+ val godot = godot
+ if (godot != null) {
+ godot.destroyAndKillProcess {
+ ProcessPhoenix.triggerRebirth(this, newInstance)
+ }
+ } else {
+ ProcessPhoenix.triggerRebirth(this, newInstance)
+ }
} else {
Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
newInstance.putExtra(EXTRA_NEW_LAUNCH, true)
@@ -343,4 +353,20 @@ open class GodotEditor : GodotActivity() {
}
}
}
+
+ override fun signApk(
+ inputPath: String,
+ outputPath: String,
+ keystorePath: String,
+ keystoreUser: String,
+ keystorePassword: String
+ ): Error {
+ val godot = godot ?: return Error.ERR_UNCONFIGURED
+ return signApk(godot.fileAccessHandler, inputPath, outputPath, keystorePath, keystoreUser, keystorePassword)
+ }
+
+ override fun verifyApk(apkPath: String): Error {
+ val godot = godot ?: return Error.ERR_UNCONFIGURED
+ return verifyApk(godot.fileAccessHandler, apkPath)
+ }
}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
index 8e4e089211..2bcfba559c 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
@@ -30,6 +30,8 @@
package org.godotengine.editor
+import org.godotengine.godot.GodotLib
+
/**
* Drives the 'run project' window of the Godot Editor.
*/
@@ -39,9 +41,9 @@ class GodotGame : GodotEditor() {
override fun overrideOrientationRequest() = false
- override fun enableLongPressGestures() = false
+ override fun enableLongPressGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click"))
- override fun enablePanAndScaleGestures() = false
+ override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"))
override fun checkForProjectPermissionsToEnable() {
// Nothing to do.. by the time we get here, the project permissions will have already
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/utils/ApkSignerUtil.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/utils/ApkSignerUtil.kt
new file mode 100644
index 0000000000..42c18c9562
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/utils/ApkSignerUtil.kt
@@ -0,0 +1,204 @@
+/**************************************************************************/
+/* ApkSignerUtil.kt */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+@file:JvmName("ApkSignerUtil")
+
+package org.godotengine.editor.utils
+
+import android.util.Log
+import com.android.apksig.ApkSigner
+import com.android.apksig.ApkVerifier
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.godotengine.godot.error.Error
+import org.godotengine.godot.io.file.FileAccessHandler
+import java.io.File
+import java.security.KeyStore
+import java.security.PrivateKey
+import java.security.Security
+import java.security.cert.X509Certificate
+import java.util.ArrayList
+
+
+/**
+ * Contains utilities methods to sign and verify Android apks using apksigner
+ */
+private const val TAG = "ApkSignerUtil"
+
+private const val DEFAULT_KEYSTORE_TYPE = "PKCS12"
+
+/**
+ * Validates that the correct version of the BouncyCastleProvider is added.
+ */
+private fun validateBouncyCastleProvider() {
+ val bcProvider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)
+ if (bcProvider !is BouncyCastleProvider) {
+ Log.v(TAG, "Removing BouncyCastleProvider $bcProvider (${bcProvider::class.java.name})")
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
+
+ val updatedBcProvider = BouncyCastleProvider()
+ val addResult = Security.addProvider(updatedBcProvider)
+ if (addResult == -1) {
+ Log.e(TAG, "Unable to add BouncyCastleProvider ${updatedBcProvider::class.java.name}")
+ } else {
+ Log.v(TAG, "Updated BouncyCastleProvider to $updatedBcProvider (${updatedBcProvider::class.java.name})")
+ }
+ }
+}
+
+/**
+ * Verifies the given Android apk
+ *
+ * @return true if verification was successful, false otherwise.
+ */
+internal fun verifyApk(fileAccessHandler: FileAccessHandler, apkPath: String): Error {
+ if (!fileAccessHandler.fileExists(apkPath)) {
+ Log.e(TAG, "Unable to access apk $apkPath")
+ return Error.ERR_FILE_NOT_FOUND
+ }
+
+ try {
+ val apkVerifier = ApkVerifier.Builder(File(apkPath)).build()
+
+ Log.v(TAG, "Verifying apk $apkPath")
+ val result = apkVerifier.verify()
+
+ Log.v(TAG, "Verification result: ${result.isVerified}")
+ return if (result.isVerified) {
+ Error.OK
+ } else {
+ Error.FAILED
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error occurred during verification for $apkPath", e)
+ return Error.ERR_INVALID_DATA
+ }
+}
+
+/**
+ * Signs the given Android apk
+ *
+ * @return true if signing is successful, false otherwise.
+ */
+internal fun signApk(fileAccessHandler: FileAccessHandler,
+ inputPath: String,
+ outputPath: String,
+ keystorePath: String,
+ keystoreUser: String,
+ keystorePassword: String,
+ keystoreType: String = DEFAULT_KEYSTORE_TYPE): Error {
+ if (!fileAccessHandler.fileExists(inputPath)) {
+ Log.e(TAG, "Unable to access input path $inputPath")
+ return Error.ERR_FILE_NOT_FOUND
+ }
+
+ val tmpOutputPath = if (outputPath != inputPath) { outputPath } else { "$outputPath.signed" }
+ if (!fileAccessHandler.canAccess(tmpOutputPath)) {
+ Log.e(TAG, "Unable to access output path $tmpOutputPath")
+ return Error.ERR_FILE_NO_PERMISSION
+ }
+
+ if (!fileAccessHandler.fileExists(keystorePath) ||
+ keystoreUser.isBlank() ||
+ keystorePassword.isBlank()) {
+ Log.e(TAG, "Invalid keystore credentials")
+ return Error.ERR_INVALID_PARAMETER
+ }
+
+ validateBouncyCastleProvider()
+
+ // 1. Obtain a KeyStore implementation
+ val keyStore = KeyStore.getInstance(keystoreType)
+
+ // 2. Load the keystore
+ val inputStream = fileAccessHandler.getInputStream(keystorePath)
+ if (inputStream == null) {
+ Log.e(TAG, "Unable to retrieve input stream from $keystorePath")
+ return Error.ERR_FILE_CANT_READ
+ }
+ try {
+ inputStream.use {
+ Log.v(TAG, "Loading keystore $keystorePath with type $keystoreType")
+ keyStore.load(it, keystorePassword.toCharArray())
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to load the keystore from $keystorePath", e)
+ return Error.ERR_FILE_CANT_READ
+ }
+
+ // 3. Load the private key and cert chain from the keystore
+ if (!keyStore.isKeyEntry(keystoreUser)) {
+ Log.e(TAG, "Key alias $keystoreUser is invalid")
+ return Error.ERR_INVALID_PARAMETER
+ }
+
+ val keyStoreKey = try {
+ keyStore.getKey(keystoreUser, keystorePassword.toCharArray())
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to recover keystore alias $keystoreUser")
+ return Error.ERR_CANT_ACQUIRE_RESOURCE
+ }
+
+ if (keyStoreKey !is PrivateKey) {
+ Log.e(TAG, "Unable to recover keystore alias $keystoreUser")
+ return Error.ERR_CANT_ACQUIRE_RESOURCE
+ }
+
+ val certChain = keyStore.getCertificateChain(keystoreUser)
+ if (certChain.isNullOrEmpty()) {
+ Log.e(TAG, "Keystore alias $keystoreUser does not contain certificates")
+ return Error.ERR_INVALID_DATA
+ }
+ val certs = ArrayList<X509Certificate>(certChain.size)
+ for (cert in certChain) {
+ certs.add(cert as X509Certificate)
+ }
+
+ val signerConfig = ApkSigner.SignerConfig.Builder(keystoreUser, keyStoreKey, certs).build()
+
+ val apkSigner = ApkSigner.Builder(listOf(signerConfig))
+ .setInputApk(File(inputPath))
+ .setOutputApk(File(tmpOutputPath))
+ .build()
+
+ try {
+ apkSigner.sign()
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to sign $inputPath", e)
+ return Error.FAILED
+ }
+
+ if (outputPath != tmpOutputPath && !fileAccessHandler.renameFile(tmpOutputPath, outputPath)) {
+ Log.e(TAG, "Unable to rename temp output file $tmpOutputPath to $outputPath")
+ return Error.ERR_FILE_CANT_WRITE
+ }
+
+ Log.v(TAG, "Signed $inputPath")
+ return Error.OK
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
index 290be727ab..49e8ffb008 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
@@ -39,8 +39,6 @@ import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Color
import android.hardware.Sensor
-import android.hardware.SensorEvent
-import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.*
import android.util.Log
@@ -52,7 +50,9 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.vending.expansion.downloader.*
+import org.godotengine.godot.error.Error
import org.godotengine.godot.input.GodotEditText
+import org.godotengine.godot.input.GodotInputHandler
import org.godotengine.godot.io.directory.DirectoryAccessHandler
import org.godotengine.godot.io.file.FileAccessHandler
import org.godotengine.godot.plugin.GodotPluginRegistry
@@ -73,6 +73,8 @@ import java.io.InputStream
import java.lang.Exception
import java.security.MessageDigest
import java.util.*
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicReference
/**
* Core component used to interface with the native layer of the engine.
@@ -80,36 +82,48 @@ import java.util.*
* Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its
* lifecycle methods are properly invoked.
*/
-class Godot(private val context: Context) : SensorEventListener {
+class Godot(private val context: Context) {
- private companion object {
+ internal companion object {
private val TAG = Godot::class.java.simpleName
- }
- private val windowManager: WindowManager by lazy {
- requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ // Supported build flavors
+ const val EDITOR_FLAVOR = "editor"
+ const val TEMPLATE_FLAVOR = "template"
+
+ /**
+ * @return true if this is an editor build, false if this is a template build
+ */
+ fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
}
+
+ private val mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
+ private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+
private val pluginRegistry: GodotPluginRegistry by lazy {
GodotPluginRegistry.getPluginRegistry()
}
- private val mSensorManager: SensorManager by lazy {
- requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager
- }
+
+ private val accelerometer_enabled = AtomicBoolean(false)
private val mAccelerometer: Sensor? by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
}
+
+ private val gravity_enabled = AtomicBoolean(false)
private val mGravity: Sensor? by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
}
+
+ private val magnetometer_enabled = AtomicBoolean(false)
private val mMagnetometer: Sensor? by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
}
+
+ private val gyroscope_enabled = AtomicBoolean(false)
private val mGyroscope: Sensor? by lazy {
mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
}
- private val mClipboard: ClipboardManager by lazy {
- requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
- }
private val uiChangeListener = View.OnSystemUiVisibilityChangeListener { visibility: Int ->
if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
@@ -126,6 +140,12 @@ class Godot(private val context: Context) : SensorEventListener {
val fileAccessHandler = FileAccessHandler(context)
val netUtils = GodotNetUtils(context)
private val commandLineFileParser = CommandLineFileParser()
+ private val godotInputHandler = GodotInputHandler(context, this)
+
+ /**
+ * Task to run when the engine terminates.
+ */
+ private val runOnTerminate = AtomicReference<Runnable>()
/**
* Tracks whether [onCreate] was completed successfully.
@@ -148,6 +168,17 @@ class Godot(private val context: Context) : SensorEventListener {
private var renderViewInitialized = false
private var primaryHost: GodotHost? = null
+ /**
+ * Tracks whether we're in the RESUMED lifecycle state.
+ * See [onResume] and [onPause]
+ */
+ private var resumed = false
+
+ /**
+ * Tracks whether [onGodotSetupCompleted] fired.
+ */
+ private val godotMainLoopStarted = AtomicBoolean(false)
+
var io: GodotIO? = null
private var commandLine : MutableList<String> = ArrayList<String>()
@@ -192,6 +223,8 @@ class Godot(private val context: Context) : SensorEventListener {
return
}
+ Log.v(TAG, "OnCreate: $primaryHost")
+
darkMode = context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
beginBenchmarkMeasure("Startup", "Godot::onCreate")
@@ -200,6 +233,8 @@ class Godot(private val context: Context) : SensorEventListener {
val activity = requireActivity()
val window = activity.window
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
+
+ Log.v(TAG, "Initializing Godot plugin registry")
GodotPluginRegistry.initializePluginRegistry(this, primaryHost.getHostPlugins(this))
if (io == null) {
io = GodotIO(activity)
@@ -323,13 +358,17 @@ class Godot(private val context: Context) : SensorEventListener {
return false
}
- if (expansionPackPath.isNotEmpty()) {
- commandLine.add("--main-pack")
- commandLine.add(expansionPackPath)
- }
- val activity = requireActivity()
- if (!nativeLayerInitializeCompleted) {
- nativeLayerInitializeCompleted = GodotLib.initialize(
+ Log.v(TAG, "OnInitNativeLayer: $host")
+
+ beginBenchmarkMeasure("Startup", "Godot::onInitNativeLayer")
+ try {
+ if (expansionPackPath.isNotEmpty()) {
+ commandLine.add("--main-pack")
+ commandLine.add(expansionPackPath)
+ }
+ val activity = requireActivity()
+ if (!nativeLayerInitializeCompleted) {
+ nativeLayerInitializeCompleted = GodotLib.initialize(
activity,
this,
activity.assets,
@@ -338,15 +377,20 @@ class Godot(private val context: Context) : SensorEventListener {
directoryAccessHandler,
fileAccessHandler,
useApkExpansion,
- )
- }
+ )
+ Log.v(TAG, "Godot native layer initialization completed: $nativeLayerInitializeCompleted")
+ }
- if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) {
- nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts)
- if (!nativeLayerSetupCompleted) {
- Log.e(TAG, "Unable to setup the Godot engine! Aborting...")
- alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit)
+ if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) {
+ nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts)
+ if (!nativeLayerSetupCompleted) {
+ throw IllegalStateException("Unable to setup the Godot engine! Aborting...")
+ } else {
+ Log.v(TAG, "Godot native layer setup completed")
+ }
}
+ } finally {
+ endBenchmarkMeasure("Startup", "Godot::onInitNativeLayer")
}
return isNativeInitialized()
}
@@ -370,6 +414,9 @@ class Godot(private val context: Context) : SensorEventListener {
throw IllegalStateException("onInitNativeLayer() must be invoked successfully prior to initializing the render view")
}
+ Log.v(TAG, "OnInitRenderView: $host")
+
+ beginBenchmarkMeasure("Startup", "Godot::onInitRenderView")
try {
val activity: Activity = host.activity
containerLayout = providedContainerLayout
@@ -392,13 +439,12 @@ class Godot(private val context: Context) : SensorEventListener {
containerLayout?.addView(editText)
renderView = if (usesVulkan()) {
if (!meetsVulkanRequirements(activity.packageManager)) {
- alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit)
- return null
+ throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message))
}
- GodotVulkanRenderView(host, this)
+ GodotVulkanRenderView(host, this, godotInputHandler)
} else {
// Fallback to openGl
- GodotGLRenderView(host, this, xrMode, useDebugOpengl)
+ GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl)
}
if (host == primaryHost) {
@@ -482,11 +528,14 @@ class Godot(private val context: Context) : SensorEventListener {
containerLayout?.removeAllViews()
containerLayout = null
}
+
+ endBenchmarkMeasure("Startup", "Godot::onInitRenderView")
}
return containerLayout
}
fun onStart(host: GodotHost) {
+ Log.v(TAG, "OnStart: $host")
if (host != primaryHost) {
return
}
@@ -495,23 +544,14 @@ class Godot(private val context: Context) : SensorEventListener {
}
fun onResume(host: GodotHost) {
+ Log.v(TAG, "OnResume: $host")
+ resumed = true
if (host != primaryHost) {
return
}
renderView?.onActivityResumed()
- if (mAccelerometer != null) {
- mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
- }
- if (mGravity != null) {
- mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME)
- }
- if (mMagnetometer != null) {
- mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME)
- }
- if (mGyroscope != null) {
- mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME)
- }
+ registerSensorsIfNeeded()
if (useImmersive) {
val window = requireActivity().window
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
@@ -526,19 +566,41 @@ class Godot(private val context: Context) : SensorEventListener {
}
}
+ private fun registerSensorsIfNeeded() {
+ if (!resumed || !godotMainLoopStarted.get()) {
+ return
+ }
+
+ if (accelerometer_enabled.get() && mAccelerometer != null) {
+ mSensorManager.registerListener(godotInputHandler, mAccelerometer, SensorManager.SENSOR_DELAY_GAME)
+ }
+ if (gravity_enabled.get() && mGravity != null) {
+ mSensorManager.registerListener(godotInputHandler, mGravity, SensorManager.SENSOR_DELAY_GAME)
+ }
+ if (magnetometer_enabled.get() && mMagnetometer != null) {
+ mSensorManager.registerListener(godotInputHandler, mMagnetometer, SensorManager.SENSOR_DELAY_GAME)
+ }
+ if (gyroscope_enabled.get() && mGyroscope != null) {
+ mSensorManager.registerListener(godotInputHandler, mGyroscope, SensorManager.SENSOR_DELAY_GAME)
+ }
+ }
+
fun onPause(host: GodotHost) {
+ Log.v(TAG, "OnPause: $host")
+ resumed = false
if (host != primaryHost) {
return
}
renderView?.onActivityPaused()
- mSensorManager.unregisterListener(this)
+ mSensorManager.unregisterListener(godotInputHandler)
for (plugin in pluginRegistry.allPlugins) {
plugin.onMainPause()
}
}
fun onStop(host: GodotHost) {
+ Log.v(TAG, "OnStop: $host")
if (host != primaryHost) {
return
}
@@ -547,6 +609,7 @@ class Godot(private val context: Context) : SensorEventListener {
}
fun onDestroy(primaryHost: GodotHost) {
+ Log.v(TAG, "OnDestroy: $primaryHost")
if (this.primaryHost != primaryHost) {
return
}
@@ -555,10 +618,7 @@ class Godot(private val context: Context) : SensorEventListener {
plugin.onMainDestroy()
}
- runOnRenderThread {
- GodotLib.ondestroy()
- forceQuit()
- }
+ renderView?.onActivityDestroyed()
}
/**
@@ -604,18 +664,22 @@ class Godot(private val context: Context) : SensorEventListener {
* Invoked on the render thread when the Godot setup is complete.
*/
private fun onGodotSetupCompleted() {
- Log.d(TAG, "OnGodotSetupCompleted")
+ Log.v(TAG, "OnGodotSetupCompleted")
// These properties are defined after Godot setup completion, so we retrieve them here.
val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click"))
val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"))
- val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis"))
+ val rotaryInputAxisValue = GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis")
runOnUiThread {
renderView?.inputHandler?.apply {
enableLongPress(longPressEnabled)
enablePanningAndScalingGestures(panScaleEnabled)
- setRotaryInputAxis(rotaryInputAxis)
+ try {
+ setRotaryInputAxis(Integer.parseInt(rotaryInputAxisValue))
+ } catch (e: NumberFormatException) {
+ Log.w(TAG, e)
+ }
}
}
@@ -629,7 +693,17 @@ class Godot(private val context: Context) : SensorEventListener {
* Invoked on the render thread when the Godot main loop has started.
*/
private fun onGodotMainLoopStarted() {
- Log.d(TAG, "OnGodotMainLoopStarted")
+ Log.v(TAG, "OnGodotMainLoopStarted")
+ godotMainLoopStarted.set(true)
+
+ accelerometer_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_accelerometer")))
+ gravity_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gravity")))
+ gyroscope_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gyroscope")))
+ magnetometer_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_magnetometer")))
+
+ runOnUiThread {
+ registerSensorsIfNeeded()
+ }
for (plugin in pluginRegistry.allPlugins) {
plugin.onGodotMainLoopStarted()
@@ -637,6 +711,15 @@ class Godot(private val context: Context) : SensorEventListener {
primaryHost?.onGodotMainLoopStarted()
}
+ /**
+ * Invoked on the render thread when the engine is about to terminate.
+ */
+ @Keep
+ private fun onGodotTerminating() {
+ Log.v(TAG, "OnGodotTerminating")
+ runOnTerminate.get()?.run()
+ }
+
private fun restart() {
primaryHost?.onGodotRestartRequested(this)
}
@@ -646,12 +729,7 @@ class Godot(private val context: Context) : SensorEventListener {
decorView.setOnSystemUiVisibilityChangeListener(uiChangeListener)
}
- @Keep
- private fun alert(message: String, title: String) {
- alert(message, title, null)
- }
-
- private fun alert(
+ fun alert(
@StringRes messageResId: Int,
@StringRes titleResId: Int,
okCallback: Runnable?
@@ -660,7 +738,9 @@ class Godot(private val context: Context) : SensorEventListener {
alert(res.getString(messageResId), res.getString(titleResId), okCallback)
}
- private fun alert(message: String, title: String, okCallback: Runnable?) {
+ @JvmOverloads
+ @Keep
+ fun alert(message: String, title: String, okCallback: Runnable? = null) {
val activity: Activity = getActivity() ?: return
runOnUiThread {
val builder = AlertDialog.Builder(activity)
@@ -770,8 +850,28 @@ class Godot(private val context: Context) : SensorEventListener {
mClipboard.setPrimaryClip(clip)
}
- private fun forceQuit() {
- forceQuit(0)
+ /**
+ * Destroys the Godot Engine and kill the process it's running in.
+ */
+ @JvmOverloads
+ fun destroyAndKillProcess(destroyRunnable: Runnable? = null) {
+ val host = primaryHost
+ val activity = host?.activity
+ if (host == null || activity == null) {
+ // Run the destroyRunnable right away as we are about to force quit.
+ destroyRunnable?.run()
+
+ // Fallback to force quit
+ forceQuit(0)
+ return
+ }
+
+ // Store the destroyRunnable so it can be run when the engine is terminating
+ runOnTerminate.set(destroyRunnable)
+
+ runOnUiThread {
+ onDestroy(host)
+ }
}
@Keep
@@ -786,11 +886,7 @@ class Godot(private val context: Context) : SensorEventListener {
} ?: return false
}
- fun onBackPressed(host: GodotHost) {
- if (host != primaryHost) {
- return
- }
-
+ fun onBackPressed() {
var shouldQuit = true
for (plugin in pluginRegistry.allPlugins) {
if (plugin.onMainBackPressed()) {
@@ -802,77 +898,6 @@ class Godot(private val context: Context) : SensorEventListener {
}
}
- private fun getRotatedValues(values: FloatArray?): FloatArray? {
- if (values == null || values.size != 3) {
- return null
- }
- val rotatedValues = FloatArray(3)
- when (windowManager.defaultDisplay.rotation) {
- Surface.ROTATION_0 -> {
- rotatedValues[0] = values[0]
- rotatedValues[1] = values[1]
- rotatedValues[2] = values[2]
- }
- Surface.ROTATION_90 -> {
- rotatedValues[0] = -values[1]
- rotatedValues[1] = values[0]
- rotatedValues[2] = values[2]
- }
- Surface.ROTATION_180 -> {
- rotatedValues[0] = -values[0]
- rotatedValues[1] = -values[1]
- rotatedValues[2] = values[2]
- }
- Surface.ROTATION_270 -> {
- rotatedValues[0] = values[1]
- rotatedValues[1] = -values[0]
- rotatedValues[2] = values[2]
- }
- }
- return rotatedValues
- }
-
- override fun onSensorChanged(event: SensorEvent) {
- if (renderView == null) {
- return
- }
-
- val rotatedValues = getRotatedValues(event.values)
-
- when (event.sensor.type) {
- Sensor.TYPE_ACCELEROMETER -> {
- rotatedValues?.let {
- renderView?.queueOnRenderThread {
- GodotLib.accelerometer(-it[0], -it[1], -it[2])
- }
- }
- }
- Sensor.TYPE_GRAVITY -> {
- rotatedValues?.let {
- renderView?.queueOnRenderThread {
- GodotLib.gravity(-it[0], -it[1], -it[2])
- }
- }
- }
- Sensor.TYPE_MAGNETIC_FIELD -> {
- rotatedValues?.let {
- renderView?.queueOnRenderThread {
- GodotLib.magnetometer(-it[0], -it[1], -it[2])
- }
- }
- }
- Sensor.TYPE_GYROSCOPE -> {
- rotatedValues?.let {
- renderView?.queueOnRenderThread {
- GodotLib.gyroscope(it[0], it[1], it[2])
- }
- }
- }
- }
- }
-
- override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
-
/**
* Used by the native code (java_godot_wrapper.h) to vibrate the device.
* @param durationMs
@@ -881,7 +906,6 @@ class Godot(private val context: Context) : SensorEventListener {
@Keep
private fun vibrate(durationMs: Int, amplitude: Int) {
if (durationMs > 0 && requestPermission("VIBRATE")) {
- val vibratorService = getActivity()?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (amplitude <= -1) {
vibratorService.vibrate(
@@ -1008,7 +1032,7 @@ class Godot(private val context: Context) : SensorEventListener {
@Keep
private fun initInputDevices() {
- renderView?.initInputDevices()
+ godotInputHandler.initInputDevices()
}
@Keep
@@ -1030,4 +1054,20 @@ class Godot(private val context: Context) : SensorEventListener {
private fun nativeDumpBenchmark(benchmarkFile: String) {
dumpBenchmark(fileAccessHandler, benchmarkFile)
}
+
+ @Keep
+ private fun nativeSignApk(inputPath: String,
+ outputPath: String,
+ keystorePath: String,
+ keystoreUser: String,
+ keystorePassword: String): Int {
+ val signResult = primaryHost?.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword) ?: Error.ERR_UNAVAILABLE
+ return signResult.toNativeValue()
+ }
+
+ @Keep
+ private fun nativeVerifyApk(apkPath: String): Int {
+ val verifyResult = primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE
+ return verifyResult.toNativeValue()
+ }
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
index 4c5e857b7a..913e3d04c5 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
@@ -85,12 +85,8 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
protected open fun getGodotAppLayout() = R.layout.godot_app_layout
override fun onDestroy() {
- Log.v(TAG, "Destroying Godot app...")
+ Log.v(TAG, "Destroying GodotActivity $this...")
super.onDestroy()
-
- godotFragment?.let {
- terminateGodotInstance(it.godot)
- }
}
override fun onGodotForceQuit(instance: Godot) {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
index a323045e1b..e0f5744368 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
@@ -30,6 +30,7 @@
package org.godotengine.godot;
+import org.godotengine.godot.error.Error;
import org.godotengine.godot.plugin.GodotPlugin;
import org.godotengine.godot.utils.BenchmarkUtils;
@@ -42,6 +43,7 @@ import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Messenger;
+import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -186,7 +188,12 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
final Activity activity = getActivity();
mCurrentIntent = activity.getIntent();
- godot = new Godot(requireContext());
+ if (parentHost != null) {
+ godot = parentHost.getGodot();
+ }
+ if (godot == null) {
+ godot = new Godot(requireContext());
+ }
performEngineInitialization();
BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate");
}
@@ -203,6 +210,12 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
if (godotContainerLayout == null) {
throw new IllegalStateException("Unable to initialize engine render view");
}
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Engine initialization failed", e);
+ final String errorMessage = TextUtils.isEmpty(e.getMessage())
+ ? getString(R.string.error_engine_setup_message)
+ : e.getMessage();
+ godot.alert(errorMessage, getString(R.string.text_error_title), godot::destroyAndKillProcess);
} catch (IllegalArgumentException ignored) {
final Activity activity = getActivity();
Intent notifierIntent = new Intent(activity, activity.getClass());
@@ -318,7 +331,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
}
public void onBackPressed() {
- godot.onBackPressed(this);
+ godot.onBackPressed();
}
/**
@@ -472,4 +485,20 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
}
return Collections.emptySet();
}
+
+ @Override
+ public Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) {
+ if (parentHost != null) {
+ return parentHost.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword);
+ }
+ return Error.ERR_UNAVAILABLE;
+ }
+
+ @Override
+ public Error verifyApk(@NonNull String apkPath) {
+ if (parentHost != null) {
+ return parentHost.verifyApk(apkPath);
+ }
+ return Error.ERR_UNAVAILABLE;
+ }
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
index 81043ce782..15a811ce83 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java
@@ -42,7 +42,6 @@ import org.godotengine.godot.xr.regular.RegularContextFactory;
import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser;
import android.annotation.SuppressLint;
-import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@@ -77,19 +76,19 @@ import java.io.InputStream;
* that matches it exactly (with regards to red/green/blue/alpha channels
* bit depths). Failure to do so would result in an EGL_BAD_MATCH error.
*/
-public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
+class GodotGLRenderView extends GLSurfaceView implements GodotRenderView {
private final GodotHost host;
private final Godot godot;
private final GodotInputHandler inputHandler;
private final GodotRenderer godotRenderer;
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
- public GodotGLRenderView(GodotHost host, Godot godot, XRMode xrMode, boolean useDebugOpengl) {
+ public GodotGLRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl) {
super(host.getActivity());
this.host = host;
this.godot = godot;
- this.inputHandler = new GodotInputHandler(this);
+ this.inputHandler = inputHandler;
this.godotRenderer = new GodotRenderer();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
@@ -103,11 +102,6 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
}
@Override
- public void initInputDevices() {
- this.inputHandler.initInputDevices();
- }
-
- @Override
public void queueOnRenderThread(Runnable event) {
queueEvent(event);
}
@@ -141,8 +135,8 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView
}
@Override
- public void onBackPressed() {
- godot.onBackPressed(host);
+ public void onActivityDestroyed() {
+ requestRenderThreadExitAndWait();
}
@Override
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java
index 1862b9fa9b..f1c84e90a7 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java
@@ -30,10 +30,13 @@
package org.godotengine.godot;
+import org.godotengine.godot.error.Error;
import org.godotengine.godot.plugin.GodotPlugin;
import android.app.Activity;
+import androidx.annotation.NonNull;
+
import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -108,4 +111,29 @@ public interface GodotHost {
default Set<GodotPlugin> getHostPlugins(Godot engine) {
return Collections.emptySet();
}
+
+ /**
+ * Signs the given Android apk
+ *
+ * @param inputPath Path to the apk that should be signed
+ * @param outputPath Path for the signed output apk; can be the same as inputPath
+ * @param keystorePath Path to the keystore to use for signing the apk
+ * @param keystoreUser Keystore user credential
+ * @param keystorePassword Keystore password credential
+ *
+ * @return {@link Error#OK} if signing is successful
+ */
+ default Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) {
+ return Error.ERR_UNAVAILABLE;
+ }
+
+ /**
+ * Verifies the given Android apk is signed
+ *
+ * @param apkPath Path to the apk that should be verified
+ * @return {@link Error#OK} if verification was successful
+ */
+ default Error verifyApk(@NonNull String apkPath) {
+ return Error.ERR_UNAVAILABLE;
+ }
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
index 4b51bd778d..219631284a 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
@@ -121,7 +121,7 @@ public class GodotIO {
activity.startActivity(intent);
return 0;
- } catch (ActivityNotFoundException e) {
+ } catch (Exception e) {
Log.e(TAG, "Unable to open uri " + uriString, e);
return 1;
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
index d0c3d4a687..295a4a6340 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
@@ -240,4 +240,15 @@ public class GodotLib {
* @see GodotRenderer#onActivityPaused()
*/
public static native void onRendererPaused();
+
+ /**
+ * @return true if input must be dispatched from the render thread. If false, input is
+ * dispatched from the UI thread.
+ */
+ public static native boolean shouldDispatchInputToRenderThread();
+
+ /**
+ * @return the project resource directory
+ */
+ public static native String getProjectResourceDir();
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java
index 5b2f9f57c7..30821eaa8e 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java
@@ -37,13 +37,14 @@ import android.view.SurfaceView;
public interface GodotRenderView {
SurfaceView getView();
- void initInputDevices();
-
/**
* Starts the thread that will drive Godot's rendering.
*/
void startRenderer();
+ /**
+ * Queues a runnable to be run on the rendering thread.
+ */
void queueOnRenderThread(Runnable event);
void onActivityPaused();
@@ -54,7 +55,7 @@ public interface GodotRenderView {
void onActivityStarted();
- void onBackPressed();
+ void onActivityDestroyed();
GodotInputHandler getInputHandler();
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
index a1ee9bd6b4..d5b05913d8 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java
@@ -50,19 +50,19 @@ import androidx.annotation.Keep;
import java.io.InputStream;
-public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
+class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView {
private final GodotHost host;
private final Godot godot;
private final GodotInputHandler mInputHandler;
private final VkRenderer mRenderer;
private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>();
- public GodotVulkanRenderView(GodotHost host, Godot godot) {
+ public GodotVulkanRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler) {
super(host.getActivity());
this.host = host;
this.godot = godot;
- mInputHandler = new GodotInputHandler(this);
+ mInputHandler = inputHandler;
mRenderer = new VkRenderer();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT));
@@ -81,11 +81,6 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
}
@Override
- public void initInputDevices() {
- mInputHandler.initInputDevices();
- }
-
- @Override
public void queueOnRenderThread(Runnable event) {
queueOnVkThread(event);
}
@@ -119,8 +114,8 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV
}
@Override
- public void onBackPressed() {
- godot.onBackPressed(host);
+ public void onActivityDestroyed() {
+ requestRenderThreadExitAndWait();
}
@Override
diff --git a/platform/android/java/lib/src/org/godotengine/godot/error/Error.kt b/platform/android/java/lib/src/org/godotengine/godot/error/Error.kt
new file mode 100644
index 0000000000..00ef5ee341
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/error/Error.kt
@@ -0,0 +1,100 @@
+/**************************************************************************/
+/* Error.kt */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+package org.godotengine.godot.error
+
+/**
+ * Godot error list.
+ *
+ * This enum MUST match its native counterpart in 'core/error/error_list.h'
+ */
+enum class Error(private val description: String) {
+ OK("OK"), // (0)
+ FAILED("Failed"), ///< Generic fail error
+ ERR_UNAVAILABLE("Unavailable"), ///< What is requested is unsupported/unavailable
+ ERR_UNCONFIGURED("Unconfigured"), ///< The object being used hasn't been properly set up yet
+ ERR_UNAUTHORIZED("Unauthorized"), ///< Missing credentials for requested resource
+ ERR_PARAMETER_RANGE_ERROR("Parameter out of range"), ///< Parameter given out of range (5)
+ ERR_OUT_OF_MEMORY("Out of memory"), ///< Out of memory
+ ERR_FILE_NOT_FOUND("File not found"),
+ ERR_FILE_BAD_DRIVE("File: Bad drive"),
+ ERR_FILE_BAD_PATH("File: Bad path"),
+ ERR_FILE_NO_PERMISSION("File: Permission denied"), // (10)
+ ERR_FILE_ALREADY_IN_USE("File already in use"),
+ ERR_FILE_CANT_OPEN("Can't open file"),
+ ERR_FILE_CANT_WRITE("Can't write file"),
+ ERR_FILE_CANT_READ("Can't read file"),
+ ERR_FILE_UNRECOGNIZED("File unrecognized"), // (15)
+ ERR_FILE_CORRUPT("File corrupt"),
+ ERR_FILE_MISSING_DEPENDENCIES("Missing dependencies for file"),
+ ERR_FILE_EOF("End of file"),
+ ERR_CANT_OPEN("Can't open"), ///< Can't open a resource/socket/file
+ ERR_CANT_CREATE("Can't create"), // (20)
+ ERR_QUERY_FAILED("Query failed"),
+ ERR_ALREADY_IN_USE("Already in use"),
+ ERR_LOCKED("Locked"), ///< resource is locked
+ ERR_TIMEOUT("Timeout"),
+ ERR_CANT_CONNECT("Can't connect"), // (25)
+ ERR_CANT_RESOLVE("Can't resolve"),
+ ERR_CONNECTION_ERROR("Connection error"),
+ ERR_CANT_ACQUIRE_RESOURCE("Can't acquire resource"),
+ ERR_CANT_FORK("Can't fork"),
+ ERR_INVALID_DATA("Invalid data"), ///< Data passed is invalid (30)
+ ERR_INVALID_PARAMETER("Invalid parameter"), ///< Parameter passed is invalid
+ ERR_ALREADY_EXISTS("Already exists"), ///< When adding, item already exists
+ ERR_DOES_NOT_EXIST("Does not exist"), ///< When retrieving/erasing, if item does not exist
+ ERR_DATABASE_CANT_READ("Can't read database"), ///< database is full
+ ERR_DATABASE_CANT_WRITE("Can't write database"), ///< database is full (35)
+ ERR_COMPILATION_FAILED("Compilation failed"),
+ ERR_METHOD_NOT_FOUND("Method not found"),
+ ERR_LINK_FAILED("Link failed"),
+ ERR_SCRIPT_FAILED("Script failed"),
+ ERR_CYCLIC_LINK("Cyclic link detected"), // (40)
+ ERR_INVALID_DECLARATION("Invalid declaration"),
+ ERR_DUPLICATE_SYMBOL("Duplicate symbol"),
+ ERR_PARSE_ERROR("Parse error"),
+ ERR_BUSY("Busy"),
+ ERR_SKIP("Skip"), // (45)
+ ERR_HELP("Help"), ///< user requested help!!
+ ERR_BUG("Bug"), ///< a bug in the software certainly happened, due to a double check failing or unexpected behavior.
+ ERR_PRINTER_ON_FIRE("Printer on fire"); /// the parallel port printer is engulfed in flames
+
+ companion object {
+ internal fun fromNativeValue(nativeValue: Int): Error? {
+ return Error.entries.getOrNull(nativeValue)
+ }
+ }
+
+ internal fun toNativeValue(): Int = this.ordinal
+
+ override fun toString(): String {
+ return description
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java
index c316812404..6a4e9da699 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java
@@ -595,6 +595,15 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
protected final void resumeGLThread() {
mGLThread.onResume();
}
+
+ /**
+ * Requests the render thread to exit and block until it does.
+ */
+ protected final void requestRenderThreadExitAndWait() {
+ if (mGLThread != null) {
+ mGLThread.requestExitAndWait();
+ }
+ }
// -- GODOT end --
/**
@@ -783,6 +792,11 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
* @return true if the buffers should be swapped, false otherwise.
*/
boolean onDrawFrame(GL10 gl);
+
+ /**
+ * Invoked when the render thread is in the process of shutting down.
+ */
+ void onRenderThreadExiting();
// -- GODOT end --
}
@@ -1621,6 +1635,12 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
* clean-up everything...
*/
synchronized (sGLThreadManager) {
+ Log.d("GLThread", "Exiting render thread");
+ GLSurfaceView view = mGLSurfaceViewWeakRef.get();
+ if (view != null) {
+ view.mRenderer.onRenderThreadExiting();
+ }
+
stopEglSurfaceLocked();
stopEglContextLocked();
}
@@ -1704,15 +1724,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
mHasSurface = true;
mFinishedCreatingEglSurface = false;
sGLThreadManager.notifyAll();
- while (mWaitingForSurface
- && !mFinishedCreatingEglSurface
- && !mExited) {
- try {
- sGLThreadManager.wait();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
}
}
@@ -1723,13 +1734,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
}
mHasSurface = false;
sGLThreadManager.notifyAll();
- while((!mWaitingForSurface) && (!mExited)) {
- try {
- sGLThreadManager.wait();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
}
}
@@ -1740,16 +1744,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
}
mRequestPaused = true;
sGLThreadManager.notifyAll();
- while ((! mExited) && (! mPaused)) {
- if (LOG_PAUSE_RESUME) {
- Log.i("Main thread", "onPause waiting for mPaused.");
- }
- try {
- sGLThreadManager.wait();
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- }
- }
}
}
@@ -1762,16 +1756,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
mRequestRender = true;
mRenderComplete = false;
sGLThreadManager.notifyAll();
- while ((! mExited) && mPaused && (!mRenderComplete)) {
- if (LOG_PAUSE_RESUME) {
- Log.i("Main thread", "onResume waiting for !mPaused.");
- }
- try {
- sGLThreadManager.wait();
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- }
- }
}
}
@@ -1793,19 +1777,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback
}
sGLThreadManager.notifyAll();
-
- // Wait for thread to react to resize and render a frame
- while (! mExited && !mPaused && !mRenderComplete
- && ableToDraw()) {
- if (LOG_SURFACE) {
- Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId());
- }
- try {
- sGLThreadManager.wait();
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- }
- }
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java
index 9d44d8826c..7e5e262b2d 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java
@@ -34,6 +34,8 @@ import org.godotengine.godot.GodotLib;
import org.godotengine.godot.plugin.GodotPlugin;
import org.godotengine.godot.plugin.GodotPluginRegistry;
+import android.util.Log;
+
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
@@ -41,6 +43,8 @@ import javax.microedition.khronos.opengles.GL10;
* Godot's GL renderer implementation.
*/
public class GodotRenderer implements GLSurfaceView.Renderer {
+ private final String TAG = GodotRenderer.class.getSimpleName();
+
private final GodotPluginRegistry pluginRegistry;
private boolean activityJustResumed = false;
@@ -62,6 +66,12 @@ public class GodotRenderer implements GLSurfaceView.Renderer {
return swapBuffers;
}
+ @Override
+ public void onRenderThreadExiting() {
+ Log.d(TAG, "Destroying Godot Engine");
+ GodotLib.ondestroy();
+ }
+
public void onSurfaceChanged(GL10 gl, int width, int height) {
GodotLib.resize(null, width, height);
for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt
index 49b34a5229..2929a0a0b0 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt
@@ -44,7 +44,7 @@ import org.godotengine.godot.GodotLib
* @See https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener
* @See https://developer.android.com/reference/android/view/ScaleGestureDetector.OnScaleGestureListener
*/
-internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureListener {
+internal class GodotGestureHandler(private val inputHandler: GodotInputHandler) : SimpleOnGestureListener(), OnScaleGestureListener {
companion object {
private val TAG = GodotGestureHandler::class.java.simpleName
@@ -65,18 +65,21 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
private var lastDragY: Float = 0.0f
override fun onDown(event: MotionEvent): Boolean {
- GodotInputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap)
+ inputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap)
nextDownIsDoubleTap = false
return true
}
override fun onSingleTapUp(event: MotionEvent): Boolean {
- GodotInputHandler.handleMotionEvent(event)
+ inputHandler.handleMotionEvent(event)
return true
}
override fun onLongPress(event: MotionEvent) {
- contextClickRouter(event)
+ val toolType = GodotInputHandler.getEventToolType(event)
+ if (toolType != MotionEvent.TOOL_TYPE_MOUSE) {
+ contextClickRouter(event)
+ }
}
private fun contextClickRouter(event: MotionEvent) {
@@ -85,10 +88,10 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
}
// Cancel the previous down event
- GodotInputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL)
+ inputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL)
// Turn a context click into a single tap right mouse button click.
- GodotInputHandler.handleMouseEvent(
+ inputHandler.handleMouseEvent(
event,
MotionEvent.ACTION_DOWN,
MotionEvent.BUTTON_SECONDARY,
@@ -104,7 +107,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
if (!hasCapture) {
// Dispatch a mouse relative ACTION_UP event to signal the end of the capture
- GodotInputHandler.handleMouseEvent(MotionEvent.ACTION_UP, true)
+ inputHandler.handleMouseEvent(MotionEvent.ACTION_UP, true)
}
pointerCaptureInProgress = hasCapture
}
@@ -131,9 +134,9 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
if (contextClickInProgress || GodotInputHandler.isMouseEvent(event)) {
// This may be an ACTION_BUTTON_RELEASE event which we don't handle,
// so we convert it to an ACTION_UP event.
- GodotInputHandler.handleMouseEvent(event, MotionEvent.ACTION_UP)
+ inputHandler.handleMouseEvent(event, MotionEvent.ACTION_UP)
} else {
- GodotInputHandler.handleTouchEvent(event)
+ inputHandler.handleTouchEvent(event)
}
pointerCaptureInProgress = false
dragInProgress = false
@@ -148,7 +151,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
private fun onActionMove(event: MotionEvent): Boolean {
if (contextClickInProgress) {
- GodotInputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false)
+ inputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false)
return true
} else if (!scaleInProgress) {
// The 'onScroll' event is triggered with a long delay.
@@ -158,7 +161,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
if (lastDragX != event.getX(0) || lastDragY != event.getY(0)) {
lastDragX = event.getX(0)
lastDragY = event.getY(0)
- GodotInputHandler.handleMotionEvent(event)
+ inputHandler.handleMotionEvent(event)
return true
}
}
@@ -168,9 +171,9 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
override fun onDoubleTapEvent(event: MotionEvent): Boolean {
if (event.actionMasked == MotionEvent.ACTION_UP) {
nextDownIsDoubleTap = false
- GodotInputHandler.handleMotionEvent(event)
+ inputHandler.handleMotionEvent(event)
} else if (event.actionMasked == MotionEvent.ACTION_MOVE && !panningAndScalingEnabled) {
- GodotInputHandler.handleMotionEvent(event)
+ inputHandler.handleMotionEvent(event)
}
return true
@@ -191,7 +194,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
if (dragInProgress || lastDragX != 0.0f || lastDragY != 0.0f) {
if (originEvent != null) {
// Cancel the drag
- GodotInputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL)
+ inputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL)
}
dragInProgress = false
lastDragX = 0.0f
@@ -202,12 +205,12 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
val x = terminusEvent.x
val y = terminusEvent.y
if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress && !dragInProgress) {
- GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f)
+ inputHandler.handlePanEvent(x, y, distanceX / 5f, distanceY / 5f)
} else if (!scaleInProgress) {
dragInProgress = true
lastDragX = terminusEvent.getX(0)
lastDragY = terminusEvent.getY(0)
- GodotInputHandler.handleMotionEvent(terminusEvent)
+ inputHandler.handleMotionEvent(terminusEvent)
}
return true
}
@@ -218,11 +221,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi
}
if (detector.scaleFactor >= 0.8f && detector.scaleFactor != 1f && detector.scaleFactor <= 1.2f) {
- GodotLib.magnify(
- detector.focusX,
- detector.focusY,
- detector.scaleFactor
- )
+ inputHandler.handleMagnifyEvent(detector.focusX, detector.focusY, detector.scaleFactor)
}
return true
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java
index 83e76e49c9..fb41cd00c0 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java
@@ -32,10 +32,14 @@ package org.godotengine.godot.input;
import static org.godotengine.godot.utils.GLUtils.DEBUG;
+import org.godotengine.godot.Godot;
import org.godotengine.godot.GodotLib;
import org.godotengine.godot.GodotRenderView;
import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
import android.hardware.input.InputManager;
import android.os.Build;
import android.util.Log;
@@ -46,6 +50,10 @@ import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
import java.util.Collections;
import java.util.HashSet;
@@ -54,7 +62,7 @@ import java.util.Set;
/**
* Handles input related events for the {@link GodotRenderView} view.
*/
-public class GodotInputHandler implements InputManager.InputDeviceListener {
+public class GodotInputHandler implements InputManager.InputDeviceListener, SensorEventListener {
private static final String TAG = GodotInputHandler.class.getSimpleName();
private static final int ROTARY_INPUT_VERTICAL_AXIS = 1;
@@ -64,8 +72,9 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4);
private final HashSet<Integer> mHardwareKeyboardIds = new HashSet<>();
- private final GodotRenderView mRenderView;
+ private final Godot godot;
private final InputManager mInputManager;
+ private final WindowManager windowManager;
private final GestureDetector gestureDetector;
private final ScaleGestureDetector scaleGestureDetector;
private final GodotGestureHandler godotGestureHandler;
@@ -75,15 +84,16 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
*/
private int lastSeenToolType = MotionEvent.TOOL_TYPE_UNKNOWN;
- private static int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS;
+ private int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS;
- public GodotInputHandler(GodotRenderView godotView) {
- final Context context = godotView.getView().getContext();
- mRenderView = godotView;
+ public GodotInputHandler(Context context, Godot godot) {
+ this.godot = godot;
mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
mInputManager.registerInputDeviceListener(this, null);
- this.godotGestureHandler = new GodotGestureHandler();
+ windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+
+ this.godotGestureHandler = new GodotGestureHandler(this);
this.gestureDetector = new GestureDetector(context, godotGestureHandler);
this.gestureDetector.setIsLongpressEnabled(false);
this.scaleGestureDetector = new ScaleGestureDetector(context, godotGestureHandler);
@@ -109,6 +119,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
}
/**
+ * @return true if input must be dispatched from the render thread. If false, input is
+ * dispatched from the UI thread.
+ */
+ private boolean shouldDispatchInputToRenderThread() {
+ return GodotLib.shouldDispatchInputToRenderThread();
+ }
+
+ /**
* On Wear OS devices, sets which axis of the mouse wheel rotary input is mapped to. This is 1 (vertical axis) by default.
*/
public void setRotaryInputAxis(int axis) {
@@ -151,14 +169,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
final int button = getGodotButton(keyCode);
final int godotJoyId = mJoystickIds.get(deviceId);
- GodotLib.joybutton(godotJoyId, button, false);
+ handleJoystickButtonEvent(godotJoyId, button, false);
}
} else {
// getKeyCode(): The physical key that was pressed.
final int physical_keycode = event.getKeyCode();
final int unicode = event.getUnicodeChar();
final int key_label = event.getDisplayLabel();
- GodotLib.key(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0);
+ handleKeyEvent(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0);
};
return true;
@@ -166,7 +184,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
public boolean onKeyDown(final int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
- mRenderView.onBackPressed();
+ godot.onBackPressed();
// press 'back' button should not terminate program
//normal handle 'back' event in game logic
return true;
@@ -187,13 +205,13 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
if (mJoystickIds.indexOfKey(deviceId) >= 0) {
final int button = getGodotButton(keyCode);
final int godotJoyId = mJoystickIds.get(deviceId);
- GodotLib.joybutton(godotJoyId, button, true);
+ handleJoystickButtonEvent(godotJoyId, button, true);
}
} else {
final int physical_keycode = event.getKeyCode();
final int unicode = event.getUnicodeChar();
final int key_label = event.getDisplayLabel();
- GodotLib.key(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0);
+ handleKeyEvent(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0);
}
return true;
@@ -248,7 +266,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
if (joystick.axesValues.indexOfKey(axis) < 0 || (float)joystick.axesValues.get(axis) != value) {
// save value to prevent repeats
joystick.axesValues.put(axis, value);
- GodotLib.joyaxis(godotJoyId, i, value);
+ handleJoystickAxisEvent(godotJoyId, i, value);
}
}
@@ -258,7 +276,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
if (joystick.hatX != hatX || joystick.hatY != hatY) {
joystick.hatX = hatX;
joystick.hatY = hatY;
- GodotLib.joyhat(godotJoyId, hatX, hatY);
+ handleJoystickHatEvent(godotJoyId, hatX, hatY);
}
}
return true;
@@ -284,10 +302,12 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
int[] deviceIds = mInputManager.getInputDeviceIds();
for (int deviceId : deviceIds) {
InputDevice device = mInputManager.getInputDevice(deviceId);
- if (DEBUG) {
- Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName()));
+ if (device != null) {
+ if (DEBUG) {
+ Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName()));
+ }
+ onInputDeviceAdded(deviceId);
}
- onInputDeviceAdded(deviceId);
}
}
@@ -364,7 +384,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
}
mJoysticksDevices.put(deviceId, joystick);
- GodotLib.joyconnectionchanged(id, true, joystick.name);
+ handleJoystickConnectionChangedEvent(id, true, joystick.name);
}
@Override
@@ -378,7 +398,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
final int godotJoyId = mJoystickIds.get(deviceId);
mJoystickIds.delete(deviceId);
mJoysticksDevices.delete(deviceId);
- GodotLib.joyconnectionchanged(godotJoyId, false, "");
+ handleJoystickConnectionChangedEvent(godotJoyId, false, "");
}
@Override
@@ -452,7 +472,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
return button;
}
- private static int getEventToolType(MotionEvent event) {
+ static int getEventToolType(MotionEvent event) {
return event.getPointerCount() > 0 ? event.getToolType(0) : MotionEvent.TOOL_TYPE_UNKNOWN;
}
@@ -482,22 +502,22 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
}
}
- static boolean handleMotionEvent(final MotionEvent event) {
+ boolean handleMotionEvent(final MotionEvent event) {
return handleMotionEvent(event, event.getActionMasked());
}
- static boolean handleMotionEvent(final MotionEvent event, int eventActionOverride) {
+ boolean handleMotionEvent(final MotionEvent event, int eventActionOverride) {
return handleMotionEvent(event, eventActionOverride, false);
}
- static boolean handleMotionEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
+ boolean handleMotionEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
if (isMouseEvent(event)) {
return handleMouseEvent(event, eventActionOverride, doubleTap);
}
return handleTouchEvent(event, eventActionOverride, doubleTap);
}
- private static float getEventTiltX(MotionEvent event) {
+ static float getEventTiltX(MotionEvent event) {
// Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise.
final float orientation = event.getOrientation();
@@ -510,7 +530,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
return (float)-Math.sin(orientation) * tiltMult;
}
- private static float getEventTiltY(MotionEvent event) {
+ static float getEventTiltY(MotionEvent event) {
// Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise.
final float orientation = event.getOrientation();
@@ -523,19 +543,19 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
return (float)Math.cos(orientation) * tiltMult;
}
- static boolean handleMouseEvent(final MotionEvent event) {
+ boolean handleMouseEvent(final MotionEvent event) {
return handleMouseEvent(event, event.getActionMasked());
}
- static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride) {
+ boolean handleMouseEvent(final MotionEvent event, int eventActionOverride) {
return handleMouseEvent(event, eventActionOverride, false);
}
- static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
+ boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
return handleMouseEvent(event, eventActionOverride, event.getButtonState(), doubleTap);
}
- static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, int buttonMaskOverride, boolean doubleTap) {
+ boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, int buttonMaskOverride, boolean doubleTap) {
final float x = event.getX();
final float y = event.getY();
@@ -564,11 +584,16 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
return handleMouseEvent(eventActionOverride, buttonMaskOverride, x, y, horizontalFactor, verticalFactor, doubleTap, sourceMouseRelative, pressure, getEventTiltX(event), getEventTiltY(event));
}
- static boolean handleMouseEvent(int eventAction, boolean sourceMouseRelative) {
+ boolean handleMouseEvent(int eventAction, boolean sourceMouseRelative) {
return handleMouseEvent(eventAction, 0, 0f, 0f, 0f, 0f, false, sourceMouseRelative, 1f, 0f, 0f);
}
- static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) {
+ boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) {
+ InputEventRunnable runnable = InputEventRunnable.obtain();
+ if (runnable == null) {
+ return false;
+ }
+
// Fix the buttonsMask
switch (eventAction) {
case MotionEvent.ACTION_CANCEL:
@@ -596,38 +621,31 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_SCROLL: {
- GodotLib.dispatchMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY);
+ runnable.setMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY);
+ dispatchInputEventRunnable(runnable);
return true;
}
}
return false;
}
- static boolean handleTouchEvent(final MotionEvent event) {
+ boolean handleTouchEvent(final MotionEvent event) {
return handleTouchEvent(event, event.getActionMasked());
}
- static boolean handleTouchEvent(final MotionEvent event, int eventActionOverride) {
+ boolean handleTouchEvent(final MotionEvent event, int eventActionOverride) {
return handleTouchEvent(event, eventActionOverride, false);
}
- static boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
- final int pointerCount = event.getPointerCount();
- if (pointerCount == 0) {
+ boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) {
+ if (event.getPointerCount() == 0) {
return true;
}
- final float[] positions = new float[pointerCount * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc...
-
- for (int i = 0; i < pointerCount; i++) {
- positions[i * 6 + 0] = event.getPointerId(i);
- positions[i * 6 + 1] = event.getX(i);
- positions[i * 6 + 2] = event.getY(i);
- positions[i * 6 + 3] = event.getPressure(i);
- positions[i * 6 + 4] = getEventTiltX(event);
- positions[i * 6 + 5] = getEventTiltY(event);
+ InputEventRunnable runnable = InputEventRunnable.obtain();
+ if (runnable == null) {
+ return false;
}
- final int actionPointerId = event.getPointerId(event.getActionIndex());
switch (eventActionOverride) {
case MotionEvent.ACTION_DOWN:
@@ -636,10 +654,137 @@ public class GodotInputHandler implements InputManager.InputDeviceListener {
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_DOWN: {
- GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap);
+ runnable.setTouchEvent(event, eventActionOverride, doubleTap);
+ dispatchInputEventRunnable(runnable);
return true;
}
}
return false;
}
+
+ void handleMagnifyEvent(float x, float y, float factor) {
+ InputEventRunnable runnable = InputEventRunnable.obtain();
+ if (runnable == null) {
+ return;
+ }
+
+ runnable.setMagnifyEvent(x, y, factor);
+ dispatchInputEventRunnable(runnable);
+ }
+
+ void handlePanEvent(float x, float y, float deltaX, float deltaY) {
+ InputEventRunnable runnable = InputEventRunnable.obtain();
+ if (runnable == null) {
+ return;
+ }
+
+ runnable.setPanEvent(x, y, deltaX, deltaY);
+ dispatchInputEventRunnable(runnable);
+ }
+
+ private void handleJoystickButtonEvent(int device, int button, boolean pressed) {
+ InputEventRunnable runnable = InputEventRunnable.obtain();
+ if (runnable == null) {
+ return;
+ }
+
+ runnable.setJoystickButtonEvent(device, button, pressed);
+ dispatchInputEventRunnable(runnable);
+ }
+
+ private void handleJoystickAxisEvent(int device, int axis, float value) {
+ InputEventRunnable runnable = InputEventRunnable.obtain();
+ if (runnable == null) {
+ return;
+ }
+
+ runnable.setJoystickAxisEvent(device, axis, value);
+ dispatchInputEventRunnable(runnable);
+ }
+
+ private void handleJoystickHatEvent(int device, int hatX, int hatY) {
+ InputEventRunnable runnable = InputEventRunnable.obtain();
+ if (runnable == null) {
+ return;
+ }
+
+ runnable.setJoystickHatEvent(device, hatX, hatY);
+ dispatchInputEventRunnable(runnable);
+ }
+
+ private void handleJoystickConnectionChangedEvent(int device, boolean connected, String name) {
+ InputEventRunnable runnable = InputEventRunnable.obtain();
+ if (runnable == null) {
+ return;
+ }
+
+ runnable.setJoystickConnectionChangedEvent(device, connected, name);
+ dispatchInputEventRunnable(runnable);
+ }
+
+ void handleKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) {
+ InputEventRunnable runnable = InputEventRunnable.obtain();
+ if (runnable == null) {
+ return;
+ }
+
+ runnable.setKeyEvent(physicalKeycode, unicode, keyLabel, pressed, echo);
+ dispatchInputEventRunnable(runnable);
+ }
+
+ private void dispatchInputEventRunnable(@NonNull InputEventRunnable runnable) {
+ if (shouldDispatchInputToRenderThread()) {
+ godot.runOnRenderThread(runnable);
+ } else {
+ runnable.run();
+ }
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ final float[] values = event.values;
+ if (values == null || values.length != 3) {
+ return;
+ }
+
+ InputEventRunnable runnable = InputEventRunnable.obtain();
+ if (runnable == null) {
+ return;
+ }
+
+ float rotatedValue0 = 0f;
+ float rotatedValue1 = 0f;
+ float rotatedValue2 = 0f;
+ switch (windowManager.getDefaultDisplay().getRotation()) {
+ case Surface.ROTATION_0:
+ rotatedValue0 = values[0];
+ rotatedValue1 = values[1];
+ rotatedValue2 = values[2];
+ break;
+
+ case Surface.ROTATION_90:
+ rotatedValue0 = -values[1];
+ rotatedValue1 = values[0];
+ rotatedValue2 = values[2];
+ break;
+
+ case Surface.ROTATION_180:
+ rotatedValue0 = -values[0];
+ rotatedValue1 = -values[1];
+ rotatedValue2 = values[2];
+ break;
+
+ case Surface.ROTATION_270:
+ rotatedValue0 = values[1];
+ rotatedValue1 = -values[0];
+ rotatedValue2 = values[2];
+ break;
+ }
+
+ runnable.setSensorEvent(event.sensor.getType(), rotatedValue0, rotatedValue1, rotatedValue2);
+ godot.runOnRenderThread(runnable);
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java
index 06b565c30f..e545669970 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java
@@ -93,8 +93,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene
@Override
public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) {
for (int i = 0; i < count; ++i) {
- GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, true, false);
- GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, false, false);
+ mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_DEL, 0, 0, true, false);
+ mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_DEL, 0, 0, false, false);
if (mHasSelection) {
mHasSelection = false;
@@ -115,8 +115,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene
// Return keys are handled through action events
continue;
}
- GodotLib.key(0, character, 0, true, false);
- GodotLib.key(0, character, 0, false, false);
+ mRenderView.getInputHandler().handleKeyEvent(0, character, 0, true, false);
+ mRenderView.getInputHandler().handleKeyEvent(0, character, 0, false, false);
}
}
@@ -127,18 +127,16 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene
if (characters != null) {
for (int i = 0; i < characters.length(); i++) {
final int character = characters.codePointAt(i);
- GodotLib.key(0, character, 0, true, false);
- GodotLib.key(0, character, 0, false, false);
+ mRenderView.getInputHandler().handleKeyEvent(0, character, 0, true, false);
+ mRenderView.getInputHandler().handleKeyEvent(0, character, 0, false, false);
}
}
}
if (pActionID == EditorInfo.IME_ACTION_DONE) {
// Enter key has been pressed
- mRenderView.queueOnRenderThread(() -> {
- GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, true, false);
- GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, false, false);
- });
+ mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, true, false);
+ mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, false, false);
mRenderView.getView().requestFocus();
return true;
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java
new file mode 100644
index 0000000000..a282791b2e
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java
@@ -0,0 +1,353 @@
+/**************************************************************************/
+/* InputEventRunnable.java */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+package org.godotengine.godot.input;
+
+import org.godotengine.godot.GodotLib;
+
+import android.hardware.Sensor;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Pools;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Used to dispatch input events.
+ *
+ * This is a specialized version of @{@link Runnable} which allows to allocate a finite pool of
+ * objects for input events dispatching, thus avoid the creation (and garbage collection) of
+ * spurious @{@link Runnable} objects.
+ */
+final class InputEventRunnable implements Runnable {
+ private static final String TAG = InputEventRunnable.class.getSimpleName();
+
+ private static final int MAX_TOUCH_POINTER_COUNT = 10; // assuming 10 fingers as max supported concurrent touch pointers
+
+ private static final Pools.Pool<InputEventRunnable> POOL = new Pools.Pool<>() {
+ private static final int MAX_POOL_SIZE = 120 * 10; // up to 120Hz input events rate for up to 5 secs (ANR limit) * 2
+
+ private final ArrayBlockingQueue<InputEventRunnable> queue = new ArrayBlockingQueue<>(MAX_POOL_SIZE);
+ private final AtomicInteger createdCount = new AtomicInteger();
+
+ @Nullable
+ @Override
+ public InputEventRunnable acquire() {
+ InputEventRunnable instance = queue.poll();
+ if (instance == null) {
+ int creationCount = createdCount.incrementAndGet();
+ if (creationCount <= MAX_POOL_SIZE) {
+ instance = new InputEventRunnable(creationCount - 1);
+ }
+ }
+
+ return instance;
+ }
+
+ @Override
+ public boolean release(@NonNull InputEventRunnable instance) {
+ return queue.offer(instance);
+ }
+ };
+
+ @Nullable
+ static InputEventRunnable obtain() {
+ InputEventRunnable runnable = POOL.acquire();
+ if (runnable == null) {
+ Log.w(TAG, "Input event pool is at capacity");
+ }
+ return runnable;
+ }
+
+ /**
+ * Used to track when this instance was created and added to the pool. Primarily used for
+ * debug purposes.
+ */
+ private final int creationRank;
+
+ private InputEventRunnable(int creationRank) {
+ this.creationRank = creationRank;
+ }
+
+ /**
+ * Set of supported input events.
+ */
+ private enum EventType {
+ MOUSE,
+ TOUCH,
+ MAGNIFY,
+ PAN,
+ JOYSTICK_BUTTON,
+ JOYSTICK_AXIS,
+ JOYSTICK_HAT,
+ JOYSTICK_CONNECTION_CHANGED,
+ KEY,
+ SENSOR
+ }
+
+ private EventType currentEventType = null;
+
+ // common event fields
+ private float eventX;
+ private float eventY;
+ private float eventDeltaX;
+ private float eventDeltaY;
+ private boolean eventPressed;
+
+ // common touch / mouse fields
+ private int eventAction;
+ private boolean doubleTap;
+
+ // Mouse event fields and setter
+ private int buttonsMask;
+ private boolean sourceMouseRelative;
+ private float pressure;
+ private float tiltX;
+ private float tiltY;
+ void setMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) {
+ this.currentEventType = EventType.MOUSE;
+ this.eventAction = eventAction;
+ this.buttonsMask = buttonsMask;
+ this.eventX = x;
+ this.eventY = y;
+ this.eventDeltaX = deltaX;
+ this.eventDeltaY = deltaY;
+ this.doubleTap = doubleClick;
+ this.sourceMouseRelative = sourceMouseRelative;
+ this.pressure = pressure;
+ this.tiltX = tiltX;
+ this.tiltY = tiltY;
+ }
+
+ // Touch event fields and setter
+ private int actionPointerId;
+ private int pointerCount;
+ private final float[] positions = new float[MAX_TOUCH_POINTER_COUNT * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc...
+ void setTouchEvent(MotionEvent event, int eventAction, boolean doubleTap) {
+ this.currentEventType = EventType.TOUCH;
+ this.eventAction = eventAction;
+ this.doubleTap = doubleTap;
+ this.actionPointerId = event.getPointerId(event.getActionIndex());
+ this.pointerCount = Math.min(event.getPointerCount(), MAX_TOUCH_POINTER_COUNT);
+ for (int i = 0; i < pointerCount; i++) {
+ positions[i * 6 + 0] = event.getPointerId(i);
+ positions[i * 6 + 1] = event.getX(i);
+ positions[i * 6 + 2] = event.getY(i);
+ positions[i * 6 + 3] = event.getPressure(i);
+ positions[i * 6 + 4] = GodotInputHandler.getEventTiltX(event);
+ positions[i * 6 + 5] = GodotInputHandler.getEventTiltY(event);
+ }
+ }
+
+ // Magnify event fields and setter
+ private float magnifyFactor;
+ void setMagnifyEvent(float x, float y, float factor) {
+ this.currentEventType = EventType.MAGNIFY;
+ this.eventX = x;
+ this.eventY = y;
+ this.magnifyFactor = factor;
+ }
+
+ // Pan event setter
+ void setPanEvent(float x, float y, float deltaX, float deltaY) {
+ this.currentEventType = EventType.PAN;
+ this.eventX = x;
+ this.eventY = y;
+ this.eventDeltaX = deltaX;
+ this.eventDeltaY = deltaY;
+ }
+
+ // common joystick field
+ private int joystickDevice;
+
+ // Joystick button event fields and setter
+ private int button;
+ void setJoystickButtonEvent(int device, int button, boolean pressed) {
+ this.currentEventType = EventType.JOYSTICK_BUTTON;
+ this.joystickDevice = device;
+ this.button = button;
+ this.eventPressed = pressed;
+ }
+
+ // Joystick axis event fields and setter
+ private int axis;
+ private float value;
+ void setJoystickAxisEvent(int device, int axis, float value) {
+ this.currentEventType = EventType.JOYSTICK_AXIS;
+ this.joystickDevice = device;
+ this.axis = axis;
+ this.value = value;
+ }
+
+ // Joystick hat event fields and setter
+ private int hatX;
+ private int hatY;
+ void setJoystickHatEvent(int device, int hatX, int hatY) {
+ this.currentEventType = EventType.JOYSTICK_HAT;
+ this.joystickDevice = device;
+ this.hatX = hatX;
+ this.hatY = hatY;
+ }
+
+ // Joystick connection changed event fields and setter
+ private boolean connected;
+ private String joystickName;
+ void setJoystickConnectionChangedEvent(int device, boolean connected, String name) {
+ this.currentEventType = EventType.JOYSTICK_CONNECTION_CHANGED;
+ this.joystickDevice = device;
+ this.connected = connected;
+ this.joystickName = name;
+ }
+
+ // Key event fields and setter
+ private int physicalKeycode;
+ private int unicode;
+ private int keyLabel;
+ private boolean echo;
+ void setKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) {
+ this.currentEventType = EventType.KEY;
+ this.physicalKeycode = physicalKeycode;
+ this.unicode = unicode;
+ this.keyLabel = keyLabel;
+ this.eventPressed = pressed;
+ this.echo = echo;
+ }
+
+ // Sensor event fields and setter
+ private int sensorType;
+ private float rotatedValue0;
+ private float rotatedValue1;
+ private float rotatedValue2;
+ void setSensorEvent(int sensorType, float rotatedValue0, float rotatedValue1, float rotatedValue2) {
+ this.currentEventType = EventType.SENSOR;
+ this.sensorType = sensorType;
+ this.rotatedValue0 = rotatedValue0;
+ this.rotatedValue1 = rotatedValue1;
+ this.rotatedValue2 = rotatedValue2;
+ }
+
+ @Override
+ public void run() {
+ try {
+ if (currentEventType == null) {
+ Log.w(TAG, "Invalid event type");
+ return;
+ }
+
+ switch (currentEventType) {
+ case MOUSE:
+ GodotLib.dispatchMouseEvent(
+ eventAction,
+ buttonsMask,
+ eventX,
+ eventY,
+ eventDeltaX,
+ eventDeltaY,
+ doubleTap,
+ sourceMouseRelative,
+ pressure,
+ tiltX,
+ tiltY);
+ break;
+
+ case TOUCH:
+ GodotLib.dispatchTouchEvent(
+ eventAction,
+ actionPointerId,
+ pointerCount,
+ positions,
+ doubleTap);
+ break;
+
+ case MAGNIFY:
+ GodotLib.magnify(eventX, eventY, magnifyFactor);
+ break;
+
+ case PAN:
+ GodotLib.pan(eventX, eventY, eventDeltaX, eventDeltaY);
+ break;
+
+ case JOYSTICK_BUTTON:
+ GodotLib.joybutton(joystickDevice, button, eventPressed);
+ break;
+
+ case JOYSTICK_AXIS:
+ GodotLib.joyaxis(joystickDevice, axis, value);
+ break;
+
+ case JOYSTICK_HAT:
+ GodotLib.joyhat(joystickDevice, hatX, hatY);
+ break;
+
+ case JOYSTICK_CONNECTION_CHANGED:
+ GodotLib.joyconnectionchanged(joystickDevice, connected, joystickName);
+ break;
+
+ case KEY:
+ GodotLib.key(physicalKeycode, unicode, keyLabel, eventPressed, echo);
+ break;
+
+ case SENSOR:
+ switch (sensorType) {
+ case Sensor.TYPE_ACCELEROMETER:
+ GodotLib.accelerometer(-rotatedValue0, -rotatedValue1, -rotatedValue2);
+ break;
+
+ case Sensor.TYPE_GRAVITY:
+ GodotLib.gravity(-rotatedValue0, -rotatedValue1, -rotatedValue2);
+ break;
+
+ case Sensor.TYPE_MAGNETIC_FIELD:
+ GodotLib.magnetometer(-rotatedValue0, -rotatedValue1, -rotatedValue2);
+ break;
+
+ case Sensor.TYPE_GYROSCOPE:
+ GodotLib.gyroscope(rotatedValue0, rotatedValue1, rotatedValue2);
+ break;
+ }
+ break;
+ }
+ } finally {
+ recycle();
+ }
+ }
+
+ /**
+ * Release the current instance back to the pool
+ */
+ private void recycle() {
+ currentEventType = null;
+ POOL.release(this);
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt
index 8ee3d5f48f..574ecd58eb 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt
@@ -34,12 +34,18 @@ import android.content.Context
import android.os.Build
import android.os.Environment
import java.io.File
+import org.godotengine.godot.GodotLib
/**
* Represents the different storage scopes.
*/
internal enum class StorageScope {
/**
+ * Covers the 'assets' directory
+ */
+ ASSETS,
+
+ /**
* Covers internal and external directories accessible to the app without restrictions.
*/
APP,
@@ -56,6 +62,10 @@ internal enum class StorageScope {
class Identifier(context: Context) {
+ companion object {
+ internal const val ASSETS_PREFIX = "assets://"
+ }
+
private val internalAppDir: String? = context.filesDir.canonicalPath
private val internalCacheDir: String? = context.cacheDir.canonicalPath
private val externalAppDir: String? = context.getExternalFilesDir(null)?.canonicalPath
@@ -64,6 +74,14 @@ internal enum class StorageScope {
private val documentsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath
/**
+ * Determine if the given path is accessible.
+ */
+ fun canAccess(path: String?): Boolean {
+ val storageScope = identifyStorageScope(path)
+ return storageScope == APP || storageScope == SHARED
+ }
+
+ /**
* Determines which [StorageScope] the given path falls under.
*/
fun identifyStorageScope(path: String?): StorageScope {
@@ -71,9 +89,16 @@ internal enum class StorageScope {
return UNKNOWN
}
- val pathFile = File(path)
+ if (path.startsWith(ASSETS_PREFIX)) {
+ return ASSETS
+ }
+
+ var pathFile = File(path)
if (!pathFile.isAbsolute) {
- return UNKNOWN
+ pathFile = File(GodotLib.getProjectResourceDir(), path)
+ if (!pathFile.isAbsolute) {
+ return UNKNOWN
+ }
}
// If we have 'All Files Access' permission, we can access all directories without
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt
index b9b7ebac6e..523e852518 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt
@@ -33,18 +33,30 @@ package org.godotengine.godot.io.directory
import android.content.Context
import android.util.Log
import android.util.SparseArray
+import org.godotengine.godot.io.StorageScope
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
+import org.godotengine.godot.io.file.AssetData
import java.io.File
import java.io.IOException
/**
* Handles directories access within the Android assets directory.
*/
-internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.DirectoryAccess {
+internal class AssetsDirectoryAccess(private val context: Context) : DirectoryAccessHandler.DirectoryAccess {
companion object {
private val TAG = AssetsDirectoryAccess::class.java.simpleName
+
+ internal fun getAssetsPath(originalPath: String): String {
+ if (originalPath.startsWith(File.separator)) {
+ return originalPath.substring(File.separator.length)
+ }
+ if (originalPath.startsWith(StorageScope.Identifier.ASSETS_PREFIX)) {
+ return originalPath.substring(StorageScope.Identifier.ASSETS_PREFIX.length)
+ }
+ return originalPath
+ }
}
private data class AssetDir(val path: String, val files: Array<String>, var current: Int = 0)
@@ -54,13 +66,6 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
private var lastDirId = STARTING_DIR_ID
private val dirs = SparseArray<AssetDir>()
- private fun getAssetsPath(originalPath: String): String {
- if (originalPath.startsWith(File.separatorChar)) {
- return originalPath.substring(1)
- }
- return originalPath
- }
-
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
override fun dirOpen(path: String): Int {
@@ -68,8 +73,8 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
try {
val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID
// Empty directories don't get added to the 'assets' directory, so
- // if ad.files.length > 0 ==> path is directory
- // if ad.files.length == 0 ==> path is file
+ // if files.length > 0 ==> path is directory
+ // if files.length == 0 ==> path is file
if (files.isEmpty()) {
return INVALID_DIR_ID
}
@@ -89,8 +94,8 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
try {
val files = assetManager.list(assetsPath) ?: return false
// Empty directories don't get added to the 'assets' directory, so
- // if ad.files.length > 0 ==> path is directory
- // if ad.files.length == 0 ==> path is file
+ // if files.length > 0 ==> path is directory
+ // if files.length == 0 ==> path is file
return files.isNotEmpty()
} catch (e: IOException) {
Log.e(TAG, "Exception on dirExists", e)
@@ -98,19 +103,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
}
}
- override fun fileExists(path: String): Boolean {
- val assetsPath = getAssetsPath(path)
- try {
- val files = assetManager.list(assetsPath) ?: return false
- // Empty directories don't get added to the 'assets' directory, so
- // if ad.files.length > 0 ==> path is directory
- // if ad.files.length == 0 ==> path is file
- return files.isEmpty()
- } catch (e: IOException) {
- Log.e(TAG, "Exception on fileExists", e)
- return false
- }
- }
+ override fun fileExists(path: String) = AssetData.fileExists(context, path)
override fun dirIsDir(dirId: Int): Boolean {
val ad: AssetDir = dirs[dirId]
@@ -171,7 +164,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
override fun getSpaceLeft() = 0L
- override fun rename(from: String, to: String) = false
+ override fun rename(from: String, to: String) = AssetData.rename(from, to)
- override fun remove(filename: String) = false
+ override fun remove(filename: String) = AssetData.delete(filename)
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
index dd6d5180c5..9f3461200b 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
@@ -32,7 +32,8 @@ package org.godotengine.godot.io.directory
import android.content.Context
import android.util.Log
-import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM
+import org.godotengine.godot.Godot
+import org.godotengine.godot.io.StorageScope
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES
/**
@@ -45,18 +46,82 @@ class DirectoryAccessHandler(context: Context) {
internal const val INVALID_DIR_ID = -1
internal const val STARTING_DIR_ID = 1
-
- private fun getAccessTypeFromNative(accessType: Int): AccessType? {
- return when (accessType) {
- ACCESS_RESOURCES.nativeValue -> ACCESS_RESOURCES
- ACCESS_FILESYSTEM.nativeValue -> ACCESS_FILESYSTEM
- else -> null
- }
- }
}
private enum class AccessType(val nativeValue: Int) {
- ACCESS_RESOURCES(0), ACCESS_FILESYSTEM(2)
+ ACCESS_RESOURCES(0),
+
+ /**
+ * Maps to [ACCESS_FILESYSTEM]
+ */
+ ACCESS_USERDATA(1),
+ ACCESS_FILESYSTEM(2);
+
+ fun generateDirAccessId(dirId: Int) = (dirId * DIR_ACCESS_ID_MULTIPLIER) + nativeValue
+
+ companion object {
+ const val DIR_ACCESS_ID_MULTIPLIER = 10
+
+ fun fromDirAccessId(dirAccessId: Int): Pair<AccessType?, Int> {
+ val nativeValue = dirAccessId % DIR_ACCESS_ID_MULTIPLIER
+ val dirId = dirAccessId / DIR_ACCESS_ID_MULTIPLIER
+ return Pair(fromNative(nativeValue), dirId)
+ }
+
+ private fun fromNative(nativeAccessType: Int): AccessType? {
+ for (accessType in entries) {
+ if (accessType.nativeValue == nativeAccessType) {
+ return accessType
+ }
+ }
+ return null
+ }
+
+ fun fromNative(nativeAccessType: Int, storageScope: StorageScope? = null): AccessType? {
+ val accessType = fromNative(nativeAccessType)
+ if (accessType == null) {
+ Log.w(TAG, "Unsupported access type $nativeAccessType")
+ return null
+ }
+
+ // 'Resources' access type takes precedence as it is simple to handle:
+ // if we receive a 'Resources' access type and this is a template build,
+ // we provide a 'Resources' directory handler.
+ // If this is an editor build, 'Resources' refers to the opened project resources
+ // and so we provide a 'Filesystem' directory handler.
+ if (accessType == ACCESS_RESOURCES) {
+ return if (Godot.isEditorBuild()) {
+ ACCESS_FILESYSTEM
+ } else {
+ ACCESS_RESOURCES
+ }
+ } else {
+ // We've received a 'Filesystem' or 'Userdata' access type. On Android, this
+ // may refer to:
+ // - assets directory (path has 'assets:/' prefix)
+ // - app directories
+ // - device shared directories
+ // As such we check the storage scope (if available) to figure what type of
+ // directory handler to provide
+ if (storageScope != null) {
+ val accessTypeFromStorageScope = when (storageScope) {
+ StorageScope.ASSETS -> ACCESS_RESOURCES
+ StorageScope.APP, StorageScope.SHARED -> ACCESS_FILESYSTEM
+ StorageScope.UNKNOWN -> null
+ }
+
+ if (accessTypeFromStorageScope != null) {
+ return accessTypeFromStorageScope
+ }
+ }
+ // If we're not able to infer the type of directory handler from the storage
+ // scope, we fall-back to the 'Filesystem' directory handler as it's the default
+ // for the 'Filesystem' access type.
+ // Note that ACCESS_USERDATA also maps to ACCESS_FILESYSTEM
+ return ACCESS_FILESYSTEM
+ }
+ }
+ }
}
internal interface DirectoryAccess {
@@ -76,8 +141,10 @@ class DirectoryAccessHandler(context: Context) {
fun remove(filename: String): Boolean
}
+ private val storageScopeIdentifier = StorageScope.Identifier(context)
+
private val assetsDirAccess = AssetsDirectoryAccess(context)
- private val fileSystemDirAccess = FilesystemDirectoryAccess(context)
+ private val fileSystemDirAccess = FilesystemDirectoryAccess(context, storageScopeIdentifier)
fun assetsFileExists(assetsPath: String) = assetsDirAccess.fileExists(assetsPath)
fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path)
@@ -85,24 +152,32 @@ class DirectoryAccessHandler(context: Context) {
private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId)
+ else -> fileSystemDirAccess.hasDirId(dirId)
}
}
fun dirOpen(nativeAccessType: Int, path: String?): Int {
- val accessType = getAccessTypeFromNative(nativeAccessType)
- if (path == null || accessType == null) {
+ if (path == null) {
return INVALID_DIR_ID
}
- return when (accessType) {
+ val storageScope = storageScopeIdentifier.identifyStorageScope(path)
+ val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return INVALID_DIR_ID
+
+ val dirId = when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path)
+ else -> fileSystemDirAccess.dirOpen(path)
+ }
+ if (dirId == INVALID_DIR_ID) {
+ return INVALID_DIR_ID
}
+
+ val dirAccessId = accessType.generateDirAccessId(dirId)
+ return dirAccessId
}
- fun dirNext(nativeAccessType: Int, dirId: Int): String {
- val accessType = getAccessTypeFromNative(nativeAccessType)
+ fun dirNext(dirAccessId: Int): String {
+ val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirNext: Invalid dir id: $dirId")
return ""
@@ -110,12 +185,12 @@ class DirectoryAccessHandler(context: Context) {
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId)
+ else -> fileSystemDirAccess.dirNext(dirId)
}
}
- fun dirClose(nativeAccessType: Int, dirId: Int) {
- val accessType = getAccessTypeFromNative(nativeAccessType)
+ fun dirClose(dirAccessId: Int) {
+ val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirClose: Invalid dir id: $dirId")
return
@@ -123,12 +198,12 @@ class DirectoryAccessHandler(context: Context) {
when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId)
+ else -> fileSystemDirAccess.dirClose(dirId)
}
}
- fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean {
- val accessType = getAccessTypeFromNative(nativeAccessType)
+ fun dirIsDir(dirAccessId: Int): Boolean {
+ val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirIsDir: Invalid dir id: $dirId")
return false
@@ -136,91 +211,106 @@ class DirectoryAccessHandler(context: Context) {
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId)
+ else -> fileSystemDirAccess.dirIsDir(dirId)
}
}
- fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean {
- val accessType = getAccessTypeFromNative(nativeAccessType)
+ fun isCurrentHidden(dirAccessId: Int): Boolean {
+ val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) {
return false
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId)
+ else -> fileSystemDirAccess.isCurrentHidden(dirId)
}
}
fun dirExists(nativeAccessType: Int, path: String?): Boolean {
- val accessType = getAccessTypeFromNative(nativeAccessType)
- if (path == null || accessType == null) {
+ if (path == null) {
return false
}
+ val storageScope = storageScopeIdentifier.identifyStorageScope(path)
+ val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
+
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirExists(path)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path)
+ else -> fileSystemDirAccess.dirExists(path)
}
}
fun fileExists(nativeAccessType: Int, path: String?): Boolean {
- val accessType = getAccessTypeFromNative(nativeAccessType)
- if (path == null || accessType == null) {
+ if (path == null) {
return false
}
+ val storageScope = storageScopeIdentifier.identifyStorageScope(path)
+ val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
+
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.fileExists(path)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path)
+ else -> fileSystemDirAccess.fileExists(path)
}
}
fun getDriveCount(nativeAccessType: Int): Int {
- val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0
+ val accessType = AccessType.fromNative(nativeAccessType) ?: return 0
return when(accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getDriveCount()
- ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount()
+ else -> fileSystemDirAccess.getDriveCount()
}
}
fun getDrive(nativeAccessType: Int, drive: Int): String {
- val accessType = getAccessTypeFromNative(nativeAccessType) ?: return ""
+ val accessType = AccessType.fromNative(nativeAccessType) ?: return ""
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive)
+ else -> fileSystemDirAccess.getDrive(drive)
}
}
- fun makeDir(nativeAccessType: Int, dir: String): Boolean {
- val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
+ fun makeDir(nativeAccessType: Int, dir: String?): Boolean {
+ if (dir == null) {
+ return false
+ }
+
+ val storageScope = storageScopeIdentifier.identifyStorageScope(dir)
+ val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
+
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir)
+ else -> fileSystemDirAccess.makeDir(dir)
}
}
fun getSpaceLeft(nativeAccessType: Int): Long {
- val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L
+ val accessType = AccessType.fromNative(nativeAccessType) ?: return 0L
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft()
- ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft()
+ else -> fileSystemDirAccess.getSpaceLeft()
}
}
fun rename(nativeAccessType: Int, from: String, to: String): Boolean {
- val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
+ val accessType = AccessType.fromNative(nativeAccessType) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.rename(from, to)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to)
+ else -> fileSystemDirAccess.rename(from, to)
}
}
- fun remove(nativeAccessType: Int, filename: String): Boolean {
- val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
+ fun remove(nativeAccessType: Int, filename: String?): Boolean {
+ if (filename == null) {
+ return false
+ }
+
+ val storageScope = storageScopeIdentifier.identifyStorageScope(filename)
+ val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.remove(filename)
- ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename)
+ else -> fileSystemDirAccess.remove(filename)
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt
index c8b4f79f30..2830216e12 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt
@@ -45,7 +45,7 @@ import java.io.File
/**
* Handles directories access with the internal and external filesystem.
*/
-internal class FilesystemDirectoryAccess(private val context: Context):
+internal class FilesystemDirectoryAccess(private val context: Context, private val storageScopeIdentifier: StorageScope.Identifier):
DirectoryAccessHandler.DirectoryAccess {
companion object {
@@ -54,7 +54,6 @@ internal class FilesystemDirectoryAccess(private val context: Context):
private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0)
- private val storageScopeIdentifier = StorageScope.Identifier(context)
private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
private var lastDirId = STARTING_DIR_ID
private val dirs = SparseArray<DirData>()
@@ -63,7 +62,8 @@ internal class FilesystemDirectoryAccess(private val context: Context):
// Directory access is available for shared storage on Android 11+
// On Android 10, access is also available as long as the `requestLegacyExternalStorage`
// tag is available.
- return storageScopeIdentifier.identifyStorageScope(path) != StorageScope.UNKNOWN
+ val storageScope = storageScopeIdentifier.identifyStorageScope(path)
+ return storageScope != StorageScope.UNKNOWN && storageScope != StorageScope.ASSETS
}
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt
new file mode 100644
index 0000000000..1ab739d90b
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt
@@ -0,0 +1,151 @@
+/**************************************************************************/
+/* AssetData.kt */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+package org.godotengine.godot.io.file
+
+import android.content.Context
+import android.content.res.AssetManager
+import android.util.Log
+import org.godotengine.godot.error.Error
+import org.godotengine.godot.io.directory.AssetsDirectoryAccess
+import java.io.IOException
+import java.io.InputStream
+import java.lang.UnsupportedOperationException
+import java.nio.ByteBuffer
+import java.nio.channels.Channels
+import java.nio.channels.ReadableByteChannel
+
+/**
+ * Implementation of the [DataAccess] which handles access and interaction with files in the
+ * 'assets' directory
+ */
+internal class AssetData(context: Context, private val filePath: String, accessFlag: FileAccessFlags) : DataAccess() {
+
+ companion object {
+ private val TAG = AssetData::class.java.simpleName
+
+ fun fileExists(context: Context, path: String): Boolean {
+ val assetsPath = AssetsDirectoryAccess.getAssetsPath(path)
+ try {
+ val files = context.assets.list(assetsPath) ?: return false
+ // Empty directories don't get added to the 'assets' directory, so
+ // if files.length > 0 ==> path is directory
+ // if files.length == 0 ==> path is file
+ return files.isEmpty()
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception on fileExists", e)
+ return false
+ }
+ }
+
+ fun fileLastModified(path: String) = 0L
+
+ fun delete(path: String) = false
+
+ fun rename(from: String, to: String) = false
+ }
+
+ private val inputStream: InputStream
+ internal val readChannel: ReadableByteChannel
+
+ private var position = 0L
+ private val length: Long
+
+ init {
+ if (accessFlag == FileAccessFlags.WRITE) {
+ throw UnsupportedOperationException("Writing to the 'assets' directory is not supported")
+ }
+
+ val assetsPath = AssetsDirectoryAccess.getAssetsPath(filePath)
+ inputStream = context.assets.open(assetsPath, AssetManager.ACCESS_BUFFER)
+ readChannel = Channels.newChannel(inputStream)
+
+ length = inputStream.available().toLong()
+ }
+
+ override fun close() {
+ try {
+ inputStream.close()
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception when closing file $filePath.", e)
+ }
+ }
+
+ override fun flush() {
+ Log.w(TAG, "flush() is not supported.")
+ }
+
+ override fun seek(position: Long) {
+ try {
+ inputStream.skip(position)
+
+ this.position = position
+ if (this.position > length) {
+ this.position = length
+ endOfFile = true
+ } else {
+ endOfFile = false
+ }
+
+ } catch(e: IOException) {
+ Log.w(TAG, "Exception when seeking file $filePath.", e)
+ }
+ }
+
+ override fun resize(length: Long): Error {
+ Log.w(TAG, "resize() is not supported.")
+ return Error.ERR_UNAVAILABLE
+ }
+
+ override fun position() = position
+
+ override fun size() = length
+
+ override fun read(buffer: ByteBuffer): Int {
+ return try {
+ val readBytes = readChannel.read(buffer)
+ if (readBytes == -1) {
+ endOfFile = true
+ 0
+ } else {
+ position += readBytes
+ endOfFile = position() >= size()
+ readBytes
+ }
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception while reading from $filePath.", e)
+ 0
+ }
+ }
+
+ override fun write(buffer: ByteBuffer) {
+ Log.w(TAG, "write() is not supported.")
+ }
+}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
index 11cf7b3566..73f020f249 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
@@ -33,12 +33,17 @@ package org.godotengine.godot.io.file
import android.content.Context
import android.os.Build
import android.util.Log
+import org.godotengine.godot.error.Error
import org.godotengine.godot.io.StorageScope
+import java.io.FileNotFoundException
import java.io.IOException
+import java.io.InputStream
import java.nio.ByteBuffer
+import java.nio.channels.Channels
import java.nio.channels.ClosedChannelException
import java.nio.channels.FileChannel
import java.nio.channels.NonWritableChannelException
+import kotlin.jvm.Throws
import kotlin.math.max
/**
@@ -47,11 +52,37 @@ import kotlin.math.max
* Its derived instances provide concrete implementations to handle regular file access, as well
* as file access through the media store API on versions of Android were scoped storage is enabled.
*/
-internal abstract class DataAccess(private val filePath: String) {
+internal abstract class DataAccess {
companion object {
private val TAG = DataAccess::class.java.simpleName
+ @Throws(java.lang.Exception::class, FileNotFoundException::class)
+ fun getInputStream(storageScope: StorageScope, context: Context, filePath: String): InputStream? {
+ return when(storageScope) {
+ StorageScope.ASSETS -> {
+ val assetData = AssetData(context, filePath, FileAccessFlags.READ)
+ Channels.newInputStream(assetData.readChannel)
+ }
+
+ StorageScope.APP -> {
+ val fileData = FileData(filePath, FileAccessFlags.READ)
+ Channels.newInputStream(fileData.fileChannel)
+ }
+ StorageScope.SHARED -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val mediaStoreData = MediaStoreData(context, filePath, FileAccessFlags.READ)
+ Channels.newInputStream(mediaStoreData.fileChannel)
+ } else {
+ null
+ }
+ }
+
+ StorageScope.UNKNOWN -> null
+ }
+ }
+
+ @Throws(java.lang.Exception::class, FileNotFoundException::class)
fun generateDataAccess(
storageScope: StorageScope,
context: Context,
@@ -61,6 +92,8 @@ internal abstract class DataAccess(private val filePath: String) {
return when (storageScope) {
StorageScope.APP -> FileData(filePath, accessFlag)
+ StorageScope.ASSETS -> AssetData(context, filePath, accessFlag)
+
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStoreData(context, filePath, accessFlag)
} else {
@@ -74,7 +107,13 @@ internal abstract class DataAccess(private val filePath: String) {
fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean {
return when(storageScope) {
StorageScope.APP -> FileData.fileExists(path)
- StorageScope.SHARED -> MediaStoreData.fileExists(context, path)
+ StorageScope.ASSETS -> AssetData.fileExists(context, path)
+ StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ MediaStoreData.fileExists(context, path)
+ } else {
+ false
+ }
+
StorageScope.UNKNOWN -> false
}
}
@@ -82,7 +121,13 @@ internal abstract class DataAccess(private val filePath: String) {
fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long {
return when(storageScope) {
StorageScope.APP -> FileData.fileLastModified(path)
- StorageScope.SHARED -> MediaStoreData.fileLastModified(context, path)
+ StorageScope.ASSETS -> AssetData.fileLastModified(path)
+ StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ MediaStoreData.fileLastModified(context, path)
+ } else {
+ 0L
+ }
+
StorageScope.UNKNOWN -> 0L
}
}
@@ -90,7 +135,13 @@ internal abstract class DataAccess(private val filePath: String) {
fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean {
return when(storageScope) {
StorageScope.APP -> FileData.delete(path)
- StorageScope.SHARED -> MediaStoreData.delete(context, path)
+ StorageScope.ASSETS -> AssetData.delete(path)
+ StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ MediaStoreData.delete(context, path)
+ } else {
+ false
+ }
+
StorageScope.UNKNOWN -> false
}
}
@@ -98,103 +149,120 @@ internal abstract class DataAccess(private val filePath: String) {
fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean {
return when(storageScope) {
StorageScope.APP -> FileData.rename(from, to)
- StorageScope.SHARED -> MediaStoreData.rename(context, from, to)
+ StorageScope.ASSETS -> AssetData.rename(from, to)
+ StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ MediaStoreData.rename(context, from, to)
+ } else {
+ false
+ }
+
StorageScope.UNKNOWN -> false
}
}
}
- protected abstract val fileChannel: FileChannel
internal var endOfFile = false
+ abstract fun close()
+ abstract fun flush()
+ abstract fun seek(position: Long)
+ abstract fun resize(length: Long): Error
+ abstract fun position(): Long
+ abstract fun size(): Long
+ abstract fun read(buffer: ByteBuffer): Int
+ abstract fun write(buffer: ByteBuffer)
- fun close() {
- try {
- fileChannel.close()
- } catch (e: IOException) {
- Log.w(TAG, "Exception when closing file $filePath.", e)
- }
+ fun seekFromEnd(positionFromEnd: Long) {
+ val positionFromBeginning = max(0, size() - positionFromEnd)
+ seek(positionFromBeginning)
}
- fun flush() {
- try {
- fileChannel.force(false)
- } catch (e: IOException) {
- Log.w(TAG, "Exception when flushing file $filePath.", e)
+ abstract class FileChannelDataAccess(private val filePath: String) : DataAccess() {
+ internal abstract val fileChannel: FileChannel
+
+ override fun close() {
+ try {
+ fileChannel.close()
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception when closing file $filePath.", e)
+ }
}
- }
- fun seek(position: Long) {
- try {
- fileChannel.position(position)
- endOfFile = position >= fileChannel.size()
- } catch (e: Exception) {
- Log.w(TAG, "Exception when seeking file $filePath.", e)
+ override fun flush() {
+ try {
+ fileChannel.force(false)
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception when flushing file $filePath.", e)
+ }
}
- }
- fun seekFromEnd(positionFromEnd: Long) {
- val positionFromBeginning = max(0, size() - positionFromEnd)
- seek(positionFromBeginning)
- }
+ override fun seek(position: Long) {
+ try {
+ fileChannel.position(position)
+ endOfFile = position >= fileChannel.size()
+ } catch (e: Exception) {
+ Log.w(TAG, "Exception when seeking file $filePath.", e)
+ }
+ }
- fun resize(length: Long): Int {
- return try {
- fileChannel.truncate(length)
- FileErrors.OK.nativeValue
- } catch (e: NonWritableChannelException) {
- FileErrors.FILE_CANT_OPEN.nativeValue
- } catch (e: ClosedChannelException) {
- FileErrors.FILE_CANT_OPEN.nativeValue
- } catch (e: IllegalArgumentException) {
- FileErrors.INVALID_PARAMETER.nativeValue
- } catch (e: IOException) {
- FileErrors.FAILED.nativeValue
+ override fun resize(length: Long): Error {
+ return try {
+ fileChannel.truncate(length)
+ Error.OK
+ } catch (e: NonWritableChannelException) {
+ Error.ERR_FILE_CANT_OPEN
+ } catch (e: ClosedChannelException) {
+ Error.ERR_FILE_CANT_OPEN
+ } catch (e: IllegalArgumentException) {
+ Error.ERR_INVALID_PARAMETER
+ } catch (e: IOException) {
+ Error.FAILED
+ }
}
- }
- fun position(): Long {
- return try {
- fileChannel.position()
+ override fun position(): Long {
+ return try {
+ fileChannel.position()
+ } catch (e: IOException) {
+ Log.w(
+ TAG,
+ "Exception when retrieving position for file $filePath.",
+ e
+ )
+ 0L
+ }
+ }
+
+ override fun size() = try {
+ fileChannel.size()
} catch (e: IOException) {
- Log.w(
- TAG,
- "Exception when retrieving position for file $filePath.",
- e
- )
+ Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
0L
}
- }
-
- fun size() = try {
- fileChannel.size()
- } catch (e: IOException) {
- Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
- 0L
- }
- fun read(buffer: ByteBuffer): Int {
- return try {
- val readBytes = fileChannel.read(buffer)
- endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size())
- if (readBytes == -1) {
+ override fun read(buffer: ByteBuffer): Int {
+ return try {
+ val readBytes = fileChannel.read(buffer)
+ endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size())
+ if (readBytes == -1) {
+ 0
+ } else {
+ readBytes
+ }
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception while reading from file $filePath.", e)
0
- } else {
- readBytes
}
- } catch (e: IOException) {
- Log.w(TAG, "Exception while reading from file $filePath.", e)
- 0
}
- }
- fun write(buffer: ByteBuffer) {
- try {
- val writtenBytes = fileChannel.write(buffer)
- if (writtenBytes > 0) {
- endOfFile = false
+ override fun write(buffer: ByteBuffer) {
+ try {
+ val writtenBytes = fileChannel.write(buffer)
+ if (writtenBytes > 0) {
+ endOfFile = false
+ }
+ } catch (e: IOException) {
+ Log.w(TAG, "Exception while writing to file $filePath.", e)
}
- } catch (e: IOException) {
- Log.w(TAG, "Exception while writing to file $filePath.", e)
}
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt
index 38974af753..f81127e90a 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt
@@ -76,7 +76,7 @@ internal enum class FileAccessFlags(val nativeValue: Int) {
companion object {
fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? {
- for (flag in values()) {
+ for (flag in entries) {
if (flag.nativeValue == modeFlag) {
return flag
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
index 1d773467e8..dee7aebdc3 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
@@ -33,8 +33,11 @@ package org.godotengine.godot.io.file
import android.content.Context
import android.util.Log
import android.util.SparseArray
+import org.godotengine.godot.error.Error
import org.godotengine.godot.io.StorageScope
import java.io.FileNotFoundException
+import java.io.InputStream
+import java.lang.UnsupportedOperationException
import java.nio.ByteBuffer
/**
@@ -45,8 +48,20 @@ class FileAccessHandler(val context: Context) {
companion object {
private val TAG = FileAccessHandler::class.java.simpleName
- internal const val INVALID_FILE_ID = 0
+ private const val INVALID_FILE_ID = 0
private const val STARTING_FILE_ID = 1
+ private val FILE_OPEN_FAILED = Pair(Error.FAILED, INVALID_FILE_ID)
+
+ internal fun getInputStream(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): InputStream? {
+ val storageScope = storageScopeIdentifier.identifyStorageScope(path)
+ return try {
+ path?.let {
+ DataAccess.getInputStream(storageScope, context, path)
+ }
+ } catch (e: Exception) {
+ null
+ }
+ }
internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
@@ -92,35 +107,55 @@ class FileAccessHandler(val context: Context) {
}
}
- private val storageScopeIdentifier = StorageScope.Identifier(context)
+ internal val storageScopeIdentifier = StorageScope.Identifier(context)
private val files = SparseArray<DataAccess>()
private var lastFileId = STARTING_FILE_ID
private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0
+ fun canAccess(filePath: String?): Boolean {
+ return storageScopeIdentifier.canAccess(filePath)
+ }
+
+ /**
+ * Returns a positive (> 0) file id when the operation succeeds.
+ * Otherwise, returns a negative value of [Error].
+ */
fun fileOpen(path: String?, modeFlags: Int): Int {
- val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID
- return fileOpen(path, accessFlag)
+ val (fileError, fileId) = fileOpen(path, FileAccessFlags.fromNativeModeFlags(modeFlags))
+ return if (fileError == Error.OK) {
+ fileId
+ } else {
+ // Return the negative of the [Error#toNativeValue()] value to differentiate from the
+ // positive file id.
+ -fileError.toNativeValue()
+ }
}
- internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int {
+ internal fun fileOpen(path: String?, accessFlag: FileAccessFlags?): Pair<Error, Int> {
+ if (accessFlag == null) {
+ return FILE_OPEN_FAILED
+ }
+
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
if (storageScope == StorageScope.UNKNOWN) {
- return INVALID_FILE_ID
+ return FILE_OPEN_FAILED
}
return try {
path?.let {
- val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return INVALID_FILE_ID
+ val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return FILE_OPEN_FAILED
files.put(++lastFileId, dataAccess)
- lastFileId
- } ?: INVALID_FILE_ID
+ Pair(Error.OK, lastFileId)
+ } ?: FILE_OPEN_FAILED
} catch (e: FileNotFoundException) {
- FileErrors.FILE_NOT_FOUND.nativeValue
+ Pair(Error.ERR_FILE_NOT_FOUND, INVALID_FILE_ID)
+ } catch (e: UnsupportedOperationException) {
+ Pair(Error.ERR_UNAVAILABLE, INVALID_FILE_ID)
} catch (e: Exception) {
Log.w(TAG, "Error while opening $path", e)
- INVALID_FILE_ID
+ FILE_OPEN_FAILED
}
}
@@ -172,6 +207,10 @@ class FileAccessHandler(val context: Context) {
files[fileId].flush()
}
+ fun getInputStream(path: String?) = Companion.getInputStream(context, storageScopeIdentifier, path)
+
+ fun renameFile(from: String, to: String) = Companion.renameFile(context, storageScopeIdentifier, from, to)
+
fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path)
fun fileLastModified(filepath: String?): Long {
@@ -191,10 +230,10 @@ class FileAccessHandler(val context: Context) {
fun fileResize(fileId: Int, length: Long): Int {
if (!hasFileId(fileId)) {
- return FileErrors.FAILED.nativeValue
+ return Error.FAILED.toNativeValue()
}
- return files[fileId].resize(length)
+ return files[fileId].resize(length).toNativeValue()
}
fun fileGetPosition(fileId: Int): Long {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt
index f2c0577c21..873daada3c 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt
@@ -38,7 +38,7 @@ import java.nio.channels.FileChannel
/**
* Implementation of [DataAccess] which handles regular (not scoped) file access and interactions.
*/
-internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess(filePath) {
+internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess.FileChannelDataAccess(filePath) {
companion object {
private val TAG = FileData::class.java.simpleName
@@ -53,7 +53,7 @@ internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAcc
fun fileLastModified(filepath: String): Long {
return try {
- File(filepath).lastModified()
+ File(filepath).lastModified() / 1000L
} catch (e: SecurityException) {
0L
}
@@ -80,10 +80,16 @@ internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAcc
override val fileChannel: FileChannel
init {
- if (accessFlag == FileAccessFlags.WRITE) {
- fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel
+ fileChannel = if (accessFlag == FileAccessFlags.WRITE) {
+ // Create parent directory is necessary
+ val parentDir = File(filePath).parentFile
+ if (parentDir != null && !parentDir.exists()) {
+ parentDir.mkdirs()
+ }
+
+ FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel
} else {
- fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel
+ RandomAccessFile(filePath, accessFlag.getMode()).channel
}
if (accessFlag.shouldTruncate()) {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
index 5410eed727..97362e2542 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
@@ -52,7 +52,7 @@ import java.nio.channels.FileChannel
*/
@RequiresApi(Build.VERSION_CODES.Q)
internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) :
- DataAccess(filePath) {
+ DataAccess.FileChannelDataAccess(filePath) {
private data class DataItem(
val id: Long,
@@ -203,7 +203,7 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi
}
val dataItem = result[0]
- return dataItem.dateModified.toLong()
+ return dataItem.dateModified.toLong() / 1000L
}
fun rename(context: Context, from: String, to: String): Boolean {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java
index 711bca02e7..8976dd65db 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java
@@ -43,6 +43,7 @@ import androidx.annotation.Nullable;
import java.lang.reflect.Constructor;
import java.util.Collection;
+import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -82,6 +83,9 @@ public final class GodotPluginRegistry {
* Retrieve the full set of loaded plugins.
*/
public Collection<GodotPlugin> getAllPlugins() {
+ if (registry.isEmpty()) {
+ return Collections.emptyList();
+ }
return registry.values();
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt
index 69748c0a8d..738f27e877 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt
@@ -37,6 +37,7 @@ import android.os.SystemClock
import android.os.Trace
import android.util.Log
import org.godotengine.godot.BuildConfig
+import org.godotengine.godot.error.Error
import org.godotengine.godot.io.file.FileAccessFlags
import org.godotengine.godot.io.file.FileAccessHandler
import org.json.JSONObject
@@ -81,7 +82,8 @@ fun beginBenchmarkMeasure(scope: String, label: String) {
*
* * Note: Only enabled on 'editorDev' build variant.
*/
-fun endBenchmarkMeasure(scope: String, label: String) {
+@JvmOverloads
+fun endBenchmarkMeasure(scope: String, label: String, dumpBenchmark: Boolean = false) {
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") {
return
}
@@ -93,6 +95,10 @@ fun endBenchmarkMeasure(scope: String, label: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Trace.endAsyncSection("[$scope] $label", 0)
}
+
+ if (dumpBenchmark) {
+ dumpBenchmark()
+ }
}
/**
@@ -102,11 +108,11 @@ fun endBenchmarkMeasure(scope: String, label: String) {
* * Note: Only enabled on 'editorDev' build variant.
*/
@JvmOverloads
-fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = benchmarkFile) {
+fun dumpBenchmark(fileAccessHandler: FileAccessHandler? = null, filepath: String? = benchmarkFile) {
if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") {
return
}
- if (!useBenchmark) {
+ if (!useBenchmark || benchmarkTracker.isEmpty()) {
return
}
@@ -123,8 +129,8 @@ fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = ben
Log.i(TAG, "BENCHMARK:\n$printOut")
if (fileAccessHandler != null && !filepath.isNullOrBlank()) {
- val fileId = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE)
- if (fileId != FileAccessHandler.INVALID_FILE_ID) {
+ val (fileError, fileId) = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE)
+ if (fileError == Error.OK) {
val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4)
fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray()))
fileAccessHandler.fileClose(fileId)
diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt
index 6f09f51d4c..a93a7dbe09 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt
@@ -31,11 +31,9 @@
@file:JvmName("VkRenderer")
package org.godotengine.godot.vulkan
+import android.util.Log
import android.view.Surface
-
-import org.godotengine.godot.Godot
import org.godotengine.godot.GodotLib
-import org.godotengine.godot.plugin.GodotPlugin
import org.godotengine.godot.plugin.GodotPluginRegistry
/**
@@ -52,6 +50,11 @@ import org.godotengine.godot.plugin.GodotPluginRegistry
* @see [VkSurfaceView.startRenderer]
*/
internal class VkRenderer {
+
+ companion object {
+ private val TAG = VkRenderer::class.java.simpleName
+ }
+
private val pluginRegistry: GodotPluginRegistry = GodotPluginRegistry.getPluginRegistry()
/**
@@ -101,8 +104,10 @@ internal class VkRenderer {
}
/**
- * Called when the rendering thread is destroyed and used as signal to tear down the Vulkan logic.
+ * Invoked when the render thread is in the process of shutting down.
*/
- fun onVkDestroy() {
+ fun onRenderThreadExiting() {
+ Log.d(TAG, "Destroying Godot Engine")
+ GodotLib.ondestroy()
}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt
index 791b425444..9e30de6a15 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt
@@ -113,12 +113,10 @@ open internal class VkSurfaceView(context: Context) : SurfaceView(context), Surf
}
/**
- * Tear down the rendering thread.
- *
- * Must not be called before a [VkRenderer] has been set.
+ * Requests the render thread to exit and block until it does.
*/
- fun onDestroy() {
- vkThread.blockingExit()
+ fun requestRenderThreadExitAndWait() {
+ vkThread.requestExitAndWait()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt
index 8c0065b31e..c7cb97d911 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt
@@ -75,6 +75,9 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
private fun threadExiting() {
lock.withLock {
+ Log.d(TAG, "Exiting render thread")
+ vkRenderer.onRenderThreadExiting()
+
exited = true
lockCondition.signalAll()
}
@@ -93,7 +96,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
/**
* Request the thread to exit and block until it's done.
*/
- fun blockingExit() {
+ fun requestExitAndWait() {
lock.withLock {
shouldExit = true
lockCondition.signalAll()
@@ -171,7 +174,6 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk
while (true) {
// Code path for exiting the thread loop.
if (shouldExit) {
- vkRenderer.onVkDestroy()
return
}
diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp
index 40068745d6..1114969de8 100644
--- a/platform/android/java_godot_lib_jni.cpp
+++ b/platform/android/java_godot_lib_jni.cpp
@@ -51,6 +51,7 @@
#include "core/config/project_settings.h"
#include "core/input/input.h"
#include "main/main.h"
+#include "servers/xr_server.h"
#ifdef TOOLS_ENABLED
#include "editor/editor_settings.h"
@@ -67,6 +68,13 @@ static AndroidInputHandler *input_handler = nullptr;
static GodotJavaWrapper *godot_java = nullptr;
static GodotIOJavaWrapper *godot_io_java = nullptr;
+enum StartupStep {
+ STEP_TERMINATED = -1,
+ STEP_SETUP,
+ STEP_SHOW_LOGO,
+ STEP_STARTED
+};
+
static SafeNumeric<int> step; // Shared between UI and render threads
static Size2 new_size;
@@ -76,7 +84,11 @@ static Vector3 magnetometer;
static Vector3 gyroscope;
static void _terminate(JNIEnv *env, bool p_restart = false) {
- step.set(-1); // Ensure no further steps are attempted and no further events are sent
+ if (step.get() == STEP_TERMINATED) {
+ return;
+ }
+
+ step.set(STEP_TERMINATED); // Ensure no further steps are attempted and no further events are sent
// lets cleanup
// Unregister android plugins
@@ -107,6 +119,7 @@ static void _terminate(JNIEnv *env, bool p_restart = false) {
NetSocketAndroid::terminate();
if (godot_java) {
+ godot_java->on_godot_terminating(env);
if (!restart_on_cleanup) {
if (p_restart) {
godot_java->restart(env);
@@ -203,7 +216,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, j
os_android->set_display_size(Size2i(p_width, p_height));
// No need to reset the surface during startup
- if (step.get() > 0) {
+ if (step.get() > STEP_SETUP) {
if (p_surface) {
ANativeWindow *native_window = ANativeWindow_fromSurface(env, p_surface);
os_android->set_native_window(native_window);
@@ -216,7 +229,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, j
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jobject p_surface) {
if (os_android) {
- if (step.get() == 0) {
+ if (step.get() == STEP_SETUP) {
// During startup
if (p_surface) {
ANativeWindow *native_window = ANativeWindow_fromSurface(env, p_surface);
@@ -230,7 +243,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *en
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz) {
- if (step.get() == 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
@@ -244,20 +257,37 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *e
}
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz) {
- if (step.get() == -1) {
+ if (step.get() == STEP_TERMINATED) {
return true;
}
- if (step.get() == 0) {
+ if (step.get() == STEP_SETUP) {
// Since Godot is initialized on the UI thread, main_thread_id was set to that thread's id,
// but for Godot purposes, the main thread is the one running the game loop
- Main::setup2();
+ Main::setup2(false); // The logo is shown in the next frame otherwise we run into rendering issues
input_handler = new AndroidInputHandler();
step.increment();
return true;
}
- if (step.get() == 1) {
+ if (step.get() == STEP_SHOW_LOGO) {
+ bool xr_enabled;
+ if (XRServer::get_xr_mode() == XRServer::XRMODE_DEFAULT) {
+ xr_enabled = GLOBAL_GET("xr/shaders/enabled");
+ } else {
+ xr_enabled = XRServer::get_xr_mode() == XRServer::XRMODE_ON;
+ }
+ // Unlike PCVR, there's no additional 2D screen onto which to render the boot logo,
+ // so we skip this step if xr is enabled.
+ if (!xr_enabled) {
+ Main::setup_boot_logo();
+ }
+
+ step.increment();
+ return true;
+ }
+
+ if (step.get() == STEP_STARTED) {
if (Main::start() != EXIT_SUCCESS) {
return true; // should exit instead and print the error
}
@@ -283,7 +313,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env,
// Called on the UI thread
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative, jfloat p_pressure, jfloat p_tilt_x, jfloat p_tilt_y) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
@@ -292,7 +322,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JN
// Called on the UI thread
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray position, jboolean p_double_tap) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
@@ -313,7 +343,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JN
// Called on the UI thread
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_factor) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
input_handler->process_magnify(Point2(p_x, p_y), p_factor);
@@ -321,7 +351,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env,
// Called on the UI thread
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
input_handler->process_pan(Point2(p_x, p_y), Vector2(p_delta_x, p_delta_y));
@@ -329,7 +359,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jcla
// Called on the UI thread
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
@@ -344,7 +374,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env
// Called on the UI thread
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jclass clazz, jint p_device, jint p_axis, jfloat p_value) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
@@ -359,7 +389,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env,
// Called on the UI thread
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jclass clazz, jint p_device, jint p_hat_x, jint p_hat_y) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
@@ -396,7 +426,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged(
// Called on the UI thread
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed, jboolean p_echo) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
input_handler->process_key_event(p_physical_keycode, p_unicode, p_key_label, p_pressed, p_echo);
@@ -419,7 +449,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gyroscope(JNIEnv *env
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, jclass clazz) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
@@ -427,7 +457,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env,
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
@@ -516,7 +546,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResu
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
@@ -528,7 +558,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz) {
- if (step.get() <= 0) {
+ if (step.get() <= STEP_SETUP) {
return;
}
@@ -536,4 +566,17 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIE
os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_PAUSED);
}
}
+
+JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz) {
+ Input *input = Input::get_singleton();
+ if (input) {
+ return !input->is_agile_input_event_flushing();
+ }
+ return false;
+}
+
+JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResourceDir(JNIEnv *env, jclass clazz) {
+ const String resource_dir = OS::get_singleton()->get_resource_dir();
+ return env->NewStringUTF(resource_dir.utf8().get_data());
+}
}
diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h
index f32ffc291a..2165ce264b 100644
--- a/platform/android/java_godot_lib_jni.h
+++ b/platform/android/java_godot_lib_jni.h
@@ -69,6 +69,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResu
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
+JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz);
+JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResourceDir(JNIEnv *env, jclass clazz);
}
#endif // JAVA_GODOT_LIB_JNI_H
diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp
index 6e7f5ef5a1..f1759af54a 100644
--- a/platform/android/java_godot_wrapper.cpp
+++ b/platform/android/java_godot_wrapper.cpp
@@ -76,6 +76,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;");
_on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V");
_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
+ _on_godot_terminating = p_env->GetMethodID(godot_class, "onGodotTerminating", "()V");
_create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I");
_get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;");
_begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;Ljava/lang/String;)V");
@@ -83,6 +84,8 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_dump_benchmark = p_env->GetMethodID(godot_class, "nativeDumpBenchmark", "(Ljava/lang/String;)V");
_get_gdextension_list_config_file = p_env->GetMethodID(godot_class, "getGDExtensionConfigFiles", "()[Ljava/lang/String;");
_has_feature = p_env->GetMethodID(godot_class, "hasFeature", "(Ljava/lang/String;)Z");
+ _sign_apk = p_env->GetMethodID(godot_class, "nativeSignApk", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I");
+ _verify_apk = p_env->GetMethodID(godot_class, "nativeVerifyApk", "(Ljava/lang/String;)I");
}
GodotJavaWrapper::~GodotJavaWrapper() {
@@ -136,6 +139,16 @@ void GodotJavaWrapper::on_godot_main_loop_started(JNIEnv *p_env) {
}
}
+void GodotJavaWrapper::on_godot_terminating(JNIEnv *p_env) {
+ if (_on_godot_terminating) {
+ if (p_env == nullptr) {
+ p_env = get_jni_env();
+ }
+ ERR_FAIL_NULL(p_env);
+ p_env->CallVoidMethod(godot_instance, _on_godot_terminating);
+ }
+}
+
void GodotJavaWrapper::restart(JNIEnv *p_env) {
if (_restart) {
if (p_env == nullptr) {
@@ -202,25 +215,27 @@ bool GodotJavaWrapper::has_get_clipboard() {
}
String GodotJavaWrapper::get_clipboard() {
+ String clipboard;
if (_get_clipboard) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, String());
jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_clipboard);
- return jstring_to_string(s, env);
- } else {
- return String();
+ clipboard = jstring_to_string(s, env);
+ env->DeleteLocalRef(s);
}
+ return clipboard;
}
String GodotJavaWrapper::get_input_fallback_mapping() {
+ String input_fallback_mapping;
if (_get_input_fallback_mapping) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, String());
jstring fallback_mapping = (jstring)env->CallObjectMethod(godot_instance, _get_input_fallback_mapping);
- return jstring_to_string(fallback_mapping, env);
- } else {
- return String();
+ input_fallback_mapping = jstring_to_string(fallback_mapping, env);
+ env->DeleteLocalRef(fallback_mapping);
}
+ return input_fallback_mapping;
}
bool GodotJavaWrapper::has_set_clipboard() {
@@ -313,14 +328,15 @@ Vector<String> GodotJavaWrapper::get_gdextension_list_config_file() const {
}
String GodotJavaWrapper::get_ca_certificates() const {
+ String ca_certificates;
if (_get_ca_certificates) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, String());
jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_ca_certificates);
- return jstring_to_string(s, env);
- } else {
- return String();
+ ca_certificates = jstring_to_string(s, env);
+ env->DeleteLocalRef(s);
}
+ return ca_certificates;
}
void GodotJavaWrapper::init_input_devices() {
@@ -410,3 +426,42 @@ bool GodotJavaWrapper::has_feature(const String &p_feature) const {
return false;
}
}
+
+Error GodotJavaWrapper::sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password) {
+ if (_sign_apk) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED);
+
+ jstring j_input_path = env->NewStringUTF(p_input_path.utf8().get_data());
+ jstring j_output_path = env->NewStringUTF(p_output_path.utf8().get_data());
+ jstring j_keystore_path = env->NewStringUTF(p_keystore_path.utf8().get_data());
+ jstring j_keystore_user = env->NewStringUTF(p_keystore_user.utf8().get_data());
+ jstring j_keystore_password = env->NewStringUTF(p_keystore_password.utf8().get_data());
+
+ int result = env->CallIntMethod(godot_instance, _sign_apk, j_input_path, j_output_path, j_keystore_path, j_keystore_user, j_keystore_password);
+
+ env->DeleteLocalRef(j_input_path);
+ env->DeleteLocalRef(j_output_path);
+ env->DeleteLocalRef(j_keystore_path);
+ env->DeleteLocalRef(j_keystore_user);
+ env->DeleteLocalRef(j_keystore_password);
+
+ return static_cast<Error>(result);
+ } else {
+ return ERR_UNCONFIGURED;
+ }
+}
+
+Error GodotJavaWrapper::verify_apk(const String &p_apk_path) {
+ if (_verify_apk) {
+ JNIEnv *env = get_jni_env();
+ ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED);
+
+ jstring j_apk_path = env->NewStringUTF(p_apk_path.utf8().get_data());
+ int result = env->CallIntMethod(godot_instance, _verify_apk, j_apk_path);
+ env->DeleteLocalRef(j_apk_path);
+ return static_cast<Error>(result);
+ } else {
+ return ERR_UNCONFIGURED;
+ }
+}
diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h
index e86391d4e3..6b66565981 100644
--- a/platform/android/java_godot_wrapper.h
+++ b/platform/android/java_godot_wrapper.h
@@ -68,12 +68,15 @@ private:
jmethodID _get_input_fallback_mapping = nullptr;
jmethodID _on_godot_setup_completed = nullptr;
jmethodID _on_godot_main_loop_started = nullptr;
+ jmethodID _on_godot_terminating = nullptr;
jmethodID _create_new_godot_instance = nullptr;
jmethodID _get_render_view = nullptr;
jmethodID _begin_benchmark_measure = nullptr;
jmethodID _end_benchmark_measure = nullptr;
jmethodID _dump_benchmark = nullptr;
jmethodID _has_feature = nullptr;
+ jmethodID _sign_apk = nullptr;
+ jmethodID _verify_apk = nullptr;
public:
GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
@@ -85,6 +88,7 @@ public:
void on_godot_setup_completed(JNIEnv *p_env = nullptr);
void on_godot_main_loop_started(JNIEnv *p_env = nullptr);
+ void on_godot_terminating(JNIEnv *p_env = nullptr);
void restart(JNIEnv *p_env = nullptr);
bool force_quit(JNIEnv *p_env = nullptr, int p_instance_id = 0);
void set_keep_screen_on(bool p_enabled);
@@ -114,6 +118,10 @@ public:
// Return true if the given feature is supported.
bool has_feature(const String &p_feature) const;
+
+ // Sign and verify apks
+ Error sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password);
+ Error verify_apk(const String &p_apk_path);
};
#endif // JAVA_GODOT_WRAPPER_H
diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp
index 764959eef3..7b0d3a29e9 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -775,6 +775,16 @@ void OS_Android::benchmark_dump() {
#endif
}
+#ifdef TOOLS_ENABLED
+Error OS_Android::sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password) {
+ return godot_java->sign_apk(p_input_path, p_output_path, p_keystore_path, p_keystore_user, p_keystore_password);
+}
+
+Error OS_Android::verify_apk(const String &p_apk_path) {
+ return godot_java->verify_apk(p_apk_path);
+}
+#endif
+
bool OS_Android::_check_internal_feature_support(const String &p_feature) {
if (p_feature == "macos" || p_feature == "web_ios" || p_feature == "web_macos" || p_feature == "windows") {
return false;
diff --git a/platform/android/os_android.h b/platform/android/os_android.h
index b150ef4f61..fb3cdf0d4c 100644
--- a/platform/android/os_android.h
+++ b/platform/android/os_android.h
@@ -91,6 +91,11 @@ public:
static const int DEFAULT_WINDOW_WIDTH = 800;
static const int DEFAULT_WINDOW_HEIGHT = 600;
+#ifdef TOOLS_ENABLED
+ Error sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password);
+ Error verify_apk(const String &p_apk_path);
+#endif
+
virtual void initialize_core() override;
virtual void initialize() override;
diff --git a/platform/android/rendering_context_driver_vulkan_android.cpp b/platform/android/rendering_context_driver_vulkan_android.cpp
index 9232126b04..a306a121f8 100644
--- a/platform/android/rendering_context_driver_vulkan_android.cpp
+++ b/platform/android/rendering_context_driver_vulkan_android.cpp
@@ -50,7 +50,7 @@ RenderingContextDriver::SurfaceID RenderingContextDriverVulkanAndroid::surface_c
create_info.window = wpd->window;
VkSurfaceKHR vk_surface = VK_NULL_HANDLE;
- VkResult err = vkCreateAndroidSurfaceKHR(instance_get(), &create_info, nullptr, &vk_surface);
+ VkResult err = vkCreateAndroidSurfaceKHR(instance_get(), &create_info, get_allocation_callbacks(VK_OBJECT_TYPE_SURFACE_KHR), &vk_surface);
ERR_FAIL_COND_V(err != VK_SUCCESS, SurfaceID());
Surface *surface = memnew(Surface);
diff --git a/platform/ios/detect.py b/platform/ios/detect.py
index 53b367a0a7..989a7f21f3 100644
--- a/platform/ios/detect.py
+++ b/platform/ios/detect.py
@@ -51,7 +51,8 @@ def get_flags():
"arch": "arm64",
"target": "template_debug",
"use_volk": False,
- "supported": ["mono"],
+ "metal": True,
+ "supported": ["metal", "mono"],
"builtin_pcre2_with_jit": False,
}
@@ -154,8 +155,22 @@ def configure(env: "SConsEnvironment"):
env.Prepend(CPPPATH=["#platform/ios"])
env.Append(CPPDEFINES=["IOS_ENABLED", "UNIX_ENABLED", "COREAUDIO_ENABLED"])
+ if env["metal"] and env["arch"] != "arm64":
+ # Only supported on arm64, so skip it for x86_64 builds.
+ env["metal"] = False
+
+ if env["metal"]:
+ env.AppendUnique(CPPDEFINES=["METAL_ENABLED", "RD_ENABLED"])
+ env.Prepend(
+ CPPPATH=[
+ "$IOS_SDK_PATH/System/Library/Frameworks/Metal.framework/Headers",
+ "$IOS_SDK_PATH/System/Library/Frameworks/QuartzCore.framework/Headers",
+ ]
+ )
+ env.Prepend(CPPPATH=["#thirdparty/spirv-cross"])
+
if env["vulkan"]:
- env.Append(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"])
+ env.AppendUnique(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"])
if env["opengl3"]:
env.Append(CPPDEFINES=["GLES3_ENABLED", "GLES_SILENCE_DEPRECATION"])
diff --git a/platform/ios/display_server_ios.h b/platform/ios/display_server_ios.h
index 4dded5aa29..bbb758074d 100644
--- a/platform/ios/display_server_ios.h
+++ b/platform/ios/display_server_ios.h
@@ -47,6 +47,10 @@
#include <vulkan/vulkan.h>
#endif
#endif // VULKAN_ENABLED
+
+#if defined(METAL_ENABLED)
+#include "drivers/metal/rendering_context_driver_metal.h"
+#endif // METAL_ENABLED
#endif // RD_ENABLED
#if defined(GLES3_ENABLED)
diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm
index 802fbefc0d..5a027e0196 100644
--- a/platform/ios/display_server_ios.mm
+++ b/platform/ios/display_server_ios.mm
@@ -73,6 +73,13 @@ DisplayServerIOS::DisplayServerIOS(const String &p_rendering_driver, WindowMode
#ifdef VULKAN_ENABLED
RenderingContextDriverVulkanIOS::WindowPlatformData vulkan;
#endif
+#ifdef METAL_ENABLED
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunguarded-availability"
+ // Eliminate "RenderingContextDriverMetal is only available on iOS 14.0 or newer".
+ RenderingContextDriverMetal::WindowPlatformData metal;
+#pragma clang diagnostic pop
+#endif
} wpd;
#if defined(VULKAN_ENABLED)
@@ -85,7 +92,19 @@ DisplayServerIOS::DisplayServerIOS(const String &p_rendering_driver, WindowMode
rendering_context = memnew(RenderingContextDriverVulkanIOS);
}
#endif
-
+#ifdef METAL_ENABLED
+ if (rendering_driver == "metal") {
+ if (@available(iOS 14.0, *)) {
+ layer = [AppDelegate.viewController.godotView initializeRenderingForDriver:@"metal"];
+ wpd.metal.layer = (CAMetalLayer *)layer;
+ rendering_context = memnew(RenderingContextDriverMetal);
+ } else {
+ OS::get_singleton()->alert("Metal is only supported on iOS 14.0 and later.");
+ r_error = ERR_UNAVAILABLE;
+ return;
+ }
+ }
+#endif
if (rendering_context) {
if (rendering_context->initialize() != OK) {
ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver));
@@ -172,6 +191,11 @@ Vector<String> DisplayServerIOS::get_rendering_drivers_func() {
#if defined(VULKAN_ENABLED)
drivers.push_back("vulkan");
#endif
+#if defined(METAL_ENABLED)
+ if (@available(ios 14.0, *)) {
+ drivers.push_back("metal");
+ }
+#endif
#if defined(GLES3_ENABLED)
drivers.push_back("opengl3");
#endif
diff --git a/platform/ios/doc_classes/EditorExportPlatformIOS.xml b/platform/ios/doc_classes/EditorExportPlatformIOS.xml
index 87994ef22b..1d4a944dc4 100644
--- a/platform/ios/doc_classes/EditorExportPlatformIOS.xml
+++ b/platform/ios/doc_classes/EditorExportPlatformIOS.xml
@@ -151,16 +151,16 @@
Indicates whether your app uses advertising data for tracking.
</member>
<member name="privacy/collected_data/audio_data/collected" type="bool" setter="" getter="">
- Indicates whether your app collects audio data data.
+ Indicates whether your app collects audio data.
</member>
<member name="privacy/collected_data/audio_data/collection_purposes" type="int" setter="" getter="">
The reasons your app collects audio data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url].
</member>
<member name="privacy/collected_data/audio_data/linked_to_user" type="bool" setter="" getter="">
- Indicates whether your app links audio data data to the user's identity.
+ Indicates whether your app links audio data to the user's identity.
</member>
<member name="privacy/collected_data/audio_data/used_for_tracking" type="bool" setter="" getter="">
- Indicates whether your app uses audio data data for tracking.
+ Indicates whether your app uses audio data for tracking.
</member>
<member name="privacy/collected_data/browsing_history/collected" type="bool" setter="" getter="">
Indicates whether your app collects browsing history.
diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp
index 490f73a36d..e4b5392c4e 100644
--- a/platform/ios/export/export_plugin.cpp
+++ b/platform/ios/export/export_plugin.cpp
@@ -282,6 +282,7 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));
+ // TODO(sgc): set to iOS 14.0 for Metal
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_ios_version"), "12.0"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/additional_plist_content", PROPERTY_HINT_MULTILINE_TEXT), ""));
@@ -1628,7 +1629,7 @@ Error EditorExportPlatformIOS::_copy_asset(const Ref<EditorExportPreset> &p_pres
asset_path = asset_path.path_join(framework_name);
destination_dir = p_out_dir.path_join(asset_path);
- destination = destination_dir.path_join(file_name);
+ destination = destination_dir;
// Convert to framework and copy.
Error err = _convert_to_framework(p_asset, destination, p_preset->get("application/bundle_identifier"));
@@ -2656,6 +2657,13 @@ bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref<EditorExp
}
}
+ if (GLOBAL_GET("rendering/rendering_device/driver.ios") == "metal") {
+ float version = p_preset->get("application/min_ios_version").operator String().to_float();
+ if (version < 14.0) {
+ err += TTR("Metal renderer require iOS 14+.") + "\n";
+ }
+ }
+
if (!err.is_empty()) {
r_error = err;
}
@@ -2971,6 +2979,7 @@ void EditorExportPlatformIOS::_update_preset_status() {
} else {
has_runnable_preset.clear();
}
+ devices_changed.set();
}
#endif
diff --git a/platform/ios/godot_app_delegate.h b/platform/ios/godot_app_delegate.h
index a9bfcbb0b2..85dc6bb390 100644
--- a/platform/ios/godot_app_delegate.h
+++ b/platform/ios/godot_app_delegate.h
@@ -32,7 +32,7 @@
typedef NSObject<UIApplicationDelegate> ApplicationDelegateService;
-@interface GodotApplicalitionDelegate : NSObject <UIApplicationDelegate>
+@interface GodotApplicationDelegate : NSObject <UIApplicationDelegate>
@property(class, readonly, strong) NSArray<ApplicationDelegateService *> *services;
diff --git a/platform/ios/godot_app_delegate.m b/platform/ios/godot_app_delegate.m
index 74e8705bc3..53e53cd0c6 100644
--- a/platform/ios/godot_app_delegate.m
+++ b/platform/ios/godot_app_delegate.m
@@ -32,11 +32,11 @@
#import "app_delegate.h"
-@interface GodotApplicalitionDelegate ()
+@interface GodotApplicationDelegate ()
@end
-@implementation GodotApplicalitionDelegate
+@implementation GodotApplicationDelegate
static NSMutableArray<ApplicationDelegateService *> *services = nil;
diff --git a/platform/ios/godot_view.mm b/platform/ios/godot_view.mm
index 1dddc9306e..552c4c262c 100644
--- a/platform/ios/godot_view.mm
+++ b/platform/ios/godot_view.mm
@@ -71,7 +71,7 @@ static const float earth_gravity = 9.80665;
CALayer<DisplayLayer> *layer;
- if ([driverName isEqualToString:@"vulkan"]) {
+ if ([driverName isEqualToString:@"vulkan"] || [driverName isEqualToString:@"metal"]) {
#if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR
if (@available(iOS 13, *)) {
layer = [GodotMetalLayer layer];
@@ -441,6 +441,9 @@ static const float earth_gravity = 9.80665;
UIInterfaceOrientation interfaceOrientation = UIInterfaceOrientationUnknown;
+#if __IPHONE_OS_VERSION_MAX_ALLOWED < 140000
+ interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
+#else
if (@available(iOS 13, *)) {
interfaceOrientation = [UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation;
#if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR
@@ -448,6 +451,7 @@ static const float earth_gravity = 9.80665;
interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
#endif
}
+#endif
switch (interfaceOrientation) {
case UIInterfaceOrientationLandscapeLeft: {
diff --git a/platform/ios/keyboard_input_view.mm b/platform/ios/keyboard_input_view.mm
index 8b614662b7..4067701a41 100644
--- a/platform/ios/keyboard_input_view.mm
+++ b/platform/ios/keyboard_input_view.mm
@@ -149,23 +149,18 @@
return;
}
+ NSString *substringToDelete = nil;
if (self.previousSelectedRange.length == 0) {
- // We are deleting all text before cursor if no range was selected.
- // This way any inserted or changed text will be updated.
- NSString *substringToDelete = [self.previousText substringToIndex:self.previousSelectedRange.location];
- [self deleteText:substringToDelete.length];
+ // Get previous text to delete.
+ substringToDelete = [self.previousText substringToIndex:self.previousSelectedRange.location];
} else {
- // If text was previously selected
- // we are sending only one `backspace`.
- // It will remove all text from text input.
+ // If text was previously selected we are sending only one `backspace`. It will remove all text from text input.
[self deleteText:1];
}
- NSString *substringToEnter;
-
+ NSString *substringToEnter = nil;
if (self.selectedRange.length == 0) {
- // If previous cursor had a selection
- // we have to calculate an inserted text.
+ // If previous cursor had a selection we have to calculate an inserted text.
if (self.previousSelectedRange.length != 0) {
NSInteger rangeEnd = self.selectedRange.location + self.selectedRange.length;
NSInteger rangeStart = MIN(self.previousSelectedRange.location, self.selectedRange.location);
@@ -187,7 +182,18 @@
substringToEnter = [self.text substringWithRange:self.selectedRange];
}
- [self enterText:substringToEnter];
+ NSInteger skip = 0;
+ if (substringToDelete != nil) {
+ for (NSInteger i = 0; i < MIN([substringToDelete length], [substringToEnter length]); i++) {
+ if ([substringToDelete characterAtIndex:i] == [substringToEnter characterAtIndex:i]) {
+ skip++;
+ } else {
+ break;
+ }
+ }
+ [self deleteText:[substringToDelete length] - skip]; // Delete changed part of previous text.
+ }
+ [self enterText:[substringToEnter substringFromIndex:skip]]; // Enter changed part of new text.
self.previousText = self.text;
self.previousSelectedRange = self.selectedRange;
diff --git a/platform/ios/main.m b/platform/ios/main.m
index 33b1034d98..89a00c9ae9 100644
--- a/platform/ios/main.m
+++ b/platform/ios/main.m
@@ -46,7 +46,7 @@ int main(int argc, char *argv[]) {
gargv = argv;
@autoreleasepool {
- NSString *className = NSStringFromClass([GodotApplicalitionDelegate class]);
+ NSString *className = NSStringFromClass([GodotApplicationDelegate class]);
UIApplicationMain(argc, argv, nil, className);
}
return 0;
diff --git a/platform/ios/rendering_context_driver_vulkan_ios.mm b/platform/ios/rendering_context_driver_vulkan_ios.mm
index 6a6af1bc41..8747bfd76a 100644
--- a/platform/ios/rendering_context_driver_vulkan_ios.mm
+++ b/platform/ios/rendering_context_driver_vulkan_ios.mm
@@ -50,7 +50,7 @@ RenderingContextDriver::SurfaceID RenderingContextDriverVulkanIOS::surface_creat
create_info.pLayer = *wpd->layer_ptr;
VkSurfaceKHR vk_surface = VK_NULL_HANDLE;
- VkResult err = vkCreateMetalSurfaceEXT(instance_get(), &create_info, nullptr, &vk_surface);
+ VkResult err = vkCreateMetalSurfaceEXT(instance_get(), &create_info, get_allocation_callbacks(VK_OBJECT_TYPE_SURFACE_KHR), &vk_surface);
ERR_FAIL_COND_V(err != VK_SUCCESS, SurfaceID());
Surface *surface = memnew(Surface);
diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py
index 303a88ab26..d1de760f34 100644
--- a/platform/linuxbsd/detect.py
+++ b/platform/linuxbsd/detect.py
@@ -179,6 +179,8 @@ def configure(env: "SConsEnvironment"):
env.Append(CCFLAGS=["-fsanitize-recover=memory"])
env.Append(LINKFLAGS=["-fsanitize=memory"])
+ env.Append(CCFLAGS=["-ffp-contract=off"])
+
# LTO
if env["lto"] == "auto": # Full LTO for production.
diff --git a/platform/linuxbsd/export/export_plugin.cpp b/platform/linuxbsd/export/export_plugin.cpp
index 936adddda3..0032b898d2 100644
--- a/platform/linuxbsd/export/export_plugin.cpp
+++ b/platform/linuxbsd/export/export_plugin.cpp
@@ -61,6 +61,20 @@ Error EditorExportPlatformLinuxBSD::_export_debug_script(const Ref<EditorExportP
}
Error EditorExportPlatformLinuxBSD::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
+ String custom_debug = p_preset->get("custom_template/debug");
+ String custom_release = p_preset->get("custom_template/release");
+ String arch = p_preset->get("binary_format/architecture");
+
+ String template_path = p_debug ? custom_debug : custom_release;
+ template_path = template_path.strip_edges();
+ if (!template_path.is_empty()) {
+ String exe_arch = _get_exe_arch(template_path);
+ if (arch != exe_arch) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Mismatching custom export template executable architecture, found \"%s\", expected \"%s\"."), exe_arch, arch));
+ return ERR_CANT_CREATE;
+ }
+ }
+
bool export_as_zip = p_path.ends_with("zip");
String pkg_name;
@@ -205,8 +219,76 @@ bool EditorExportPlatformLinuxBSD::is_executable(const String &p_path) const {
return is_elf(p_path) || is_shebang(p_path);
}
+bool EditorExportPlatformLinuxBSD::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
+ String err;
+ bool valid = EditorExportPlatformPC::has_valid_export_configuration(p_preset, err, r_missing_templates, p_debug);
+
+ String custom_debug = p_preset->get("custom_template/debug").operator String().strip_edges();
+ String custom_release = p_preset->get("custom_template/release").operator String().strip_edges();
+ String arch = p_preset->get("binary_format/architecture");
+
+ if (!custom_debug.is_empty() && FileAccess::exists(custom_debug)) {
+ String exe_arch = _get_exe_arch(custom_debug);
+ if (arch != exe_arch) {
+ err += vformat(TTR("Mismatching custom debug export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch) + "\n";
+ }
+ }
+ if (!custom_release.is_empty() && FileAccess::exists(custom_release)) {
+ String exe_arch = _get_exe_arch(custom_release);
+ if (arch != exe_arch) {
+ err += vformat(TTR("Mismatching custom release export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch) + "\n";
+ }
+ }
+
+ if (!err.is_empty()) {
+ r_error = err;
+ }
+
+ return valid;
+}
+
+String EditorExportPlatformLinuxBSD::_get_exe_arch(const String &p_path) const {
+ Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
+ if (f.is_null()) {
+ return "invalid";
+ }
+
+ // Read and check ELF magic number.
+ {
+ uint32_t magic = f->get_32();
+ if (magic != 0x464c457f) { // 0x7F + "ELF"
+ return "invalid";
+ }
+ }
+
+ // Process header.
+ int64_t header_pos = f->get_position();
+ f->seek(header_pos + 14);
+ uint16_t machine = f->get_16();
+ f->close();
+
+ switch (machine) {
+ case 0x0003:
+ return "x86_32";
+ case 0x003e:
+ return "x86_64";
+ case 0x0014:
+ return "ppc32";
+ case 0x0015:
+ return "ppc64";
+ case 0x0028:
+ return "arm32";
+ case 0x00b7:
+ return "arm64";
+ case 0x00f3:
+ return "rv64";
+ default:
+ return "unknown";
+ }
+}
+
Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) {
- // Patch the header of the "pck" section in the ELF file so that it corresponds to the embedded data
+ // Patch the header of the "pck" section in the ELF file so that it corresponds to the embedded data.
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ_WRITE);
if (f.is_null()) {
@@ -214,7 +296,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int
return ERR_CANT_OPEN;
}
- // Read and check ELF magic number
+ // Read and check ELF magic number.
{
uint32_t magic = f->get_32();
if (magic != 0x464c457f) { // 0x7F + "ELF"
@@ -223,7 +305,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int
}
}
- // Read program architecture bits from class field
+ // Read program architecture bits from class field.
int bits = f->get_8() * 32;
@@ -231,7 +313,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int
add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("32-bit executables cannot have embedded data >= 4 GiB."));
}
- // Get info about the section header table
+ // Get info about the section header table.
int64_t section_table_pos;
int64_t section_header_size;
@@ -249,13 +331,13 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int
int num_sections = f->get_16();
int string_section_idx = f->get_16();
- // Load the strings table
+ // Load the strings table.
uint8_t *strings;
{
- // Jump to the strings section header
+ // Jump to the strings section header.
f->seek(section_table_pos + string_section_idx * section_header_size);
- // Read strings data size and offset
+ // Read strings data size and offset.
int64_t string_data_pos;
int64_t string_data_size;
if (bits == 32) {
@@ -268,7 +350,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int
string_data_size = f->get_64();
}
- // Read strings data
+ // Read strings data.
f->seek(string_data_pos);
strings = (uint8_t *)memalloc(string_data_size);
if (!strings) {
@@ -277,7 +359,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int
f->get_buffer(strings, string_data_size);
}
- // Search for the "pck" section
+ // Search for the "pck" section.
bool found = false;
for (int i = 0; i < num_sections; ++i) {
diff --git a/platform/linuxbsd/export/export_plugin.h b/platform/linuxbsd/export/export_plugin.h
index 21bd81ed2f..bbc55b82ce 100644
--- a/platform/linuxbsd/export/export_plugin.h
+++ b/platform/linuxbsd/export/export_plugin.h
@@ -69,11 +69,13 @@ class EditorExportPlatformLinuxBSD : public EditorExportPlatformPC {
bool is_shebang(const String &p_path) const;
Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path);
+ String _get_exe_arch(const String &p_path) const;
public:
virtual void get_export_options(List<ExportOption> *r_options) const override;
virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override;
virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override;
+ virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override;
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override;
virtual String get_template_file_name(const String &p_target, const String &p_arch) const override;
virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override;
diff --git a/platform/linuxbsd/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp
index 3534c1afee..a67428b9a4 100644
--- a/platform/linuxbsd/joypad_linux.cpp
+++ b/platform/linuxbsd/joypad_linux.cpp
@@ -374,6 +374,12 @@ void JoypadLinux::open_joypad(const char *p_path) {
name = namebuf;
}
+ for (const String &word : name.to_lower().split(" ")) {
+ if (banned_words.has(word)) {
+ return;
+ }
+ }
+
if (ioctl(fd, EVIOCGID, &inpid) < 0) {
close(fd);
return;
diff --git a/platform/linuxbsd/joypad_linux.h b/platform/linuxbsd/joypad_linux.h
index 26a9908d4e..bf24d8e5a5 100644
--- a/platform/linuxbsd/joypad_linux.h
+++ b/platform/linuxbsd/joypad_linux.h
@@ -94,6 +94,21 @@ private:
Vector<String> attached_devices;
+ // List of lowercase words that will prevent the controller from being recognized if its name matches.
+ // This is done to prevent trackpads, graphics tablets and motherboard LED controllers from being
+ // recognized as controllers (and taking up controller ID slots as a result).
+ // Only whole words are matched within the controller name string. The match is case-insensitive.
+ const Vector<String> banned_words = {
+ "touchpad", // Matches e.g. "SynPS/2 Synaptics TouchPad", "Sony Interactive Entertainment DualSense Wireless Controller Touchpad"
+ "trackpad",
+ "clickpad",
+ "keyboard", // Matches e.g. "PG-90215 Keyboard", "Usb Keyboard Usb Keyboard Consumer Control"
+ "mouse", // Matches e.g. "Mouse passthrough"
+ "pen", // Matches e.g. "Wacom One by Wacom S Pen"
+ "finger", // Matches e.g. "Wacom HID 495F Finger"
+ "led", // Matches e.g. "ASRock LED Controller"
+ };
+
static void monitor_joypads_thread_func(void *p_user);
void monitor_joypads_thread_run();
diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp
index adc9beed66..93096fcdcc 100644
--- a/platform/linuxbsd/wayland/display_server_wayland.cpp
+++ b/platform/linuxbsd/wayland/display_server_wayland.cpp
@@ -1238,7 +1238,7 @@ void DisplayServerWayland::process_events() {
} else {
try_suspend();
}
- } else if (wayland_thread.get_reset_frame()) {
+ } else if (!wayland_thread.is_suspended() || wayland_thread.get_reset_frame()) {
// At last, a sign of life! We're no longer suspended.
suspended = false;
}
diff --git a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp
index c874c45a8a..0417ba95eb 100644
--- a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp
+++ b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp
@@ -51,7 +51,7 @@ RenderingContextDriver::SurfaceID RenderingContextDriverVulkanWayland::surface_c
create_info.surface = wpd->surface;
VkSurfaceKHR vk_surface = VK_NULL_HANDLE;
- VkResult err = vkCreateWaylandSurfaceKHR(instance_get(), &create_info, nullptr, &vk_surface);
+ VkResult err = vkCreateWaylandSurfaceKHR(instance_get(), &create_info, get_allocation_callbacks(VK_OBJECT_TYPE_SURFACE_KHR), &vk_surface);
ERR_FAIL_COND_V(err != VK_SUCCESS, SurfaceID());
Surface *surface = memnew(Surface);
diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp
index 341cc517e3..ab13105d18 100644
--- a/platform/linuxbsd/wayland/wayland_thread.cpp
+++ b/platform/linuxbsd/wayland/wayland_thread.cpp
@@ -1263,23 +1263,25 @@ void WaylandThread::_wl_seat_on_capabilities(void *data, struct wl_seat *wl_seat
// Pointer handling.
if (capabilities & WL_SEAT_CAPABILITY_POINTER) {
- ss->cursor_surface = wl_compositor_create_surface(ss->registry->wl_compositor);
- wl_surface_commit(ss->cursor_surface);
+ if (!ss->wl_pointer) {
+ ss->cursor_surface = wl_compositor_create_surface(ss->registry->wl_compositor);
+ wl_surface_commit(ss->cursor_surface);
- ss->wl_pointer = wl_seat_get_pointer(wl_seat);
- wl_pointer_add_listener(ss->wl_pointer, &wl_pointer_listener, ss);
+ ss->wl_pointer = wl_seat_get_pointer(wl_seat);
+ wl_pointer_add_listener(ss->wl_pointer, &wl_pointer_listener, ss);
- if (ss->registry->wp_relative_pointer_manager) {
- ss->wp_relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer(ss->registry->wp_relative_pointer_manager, ss->wl_pointer);
- zwp_relative_pointer_v1_add_listener(ss->wp_relative_pointer, &wp_relative_pointer_listener, ss);
- }
+ if (ss->registry->wp_relative_pointer_manager) {
+ ss->wp_relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer(ss->registry->wp_relative_pointer_manager, ss->wl_pointer);
+ zwp_relative_pointer_v1_add_listener(ss->wp_relative_pointer, &wp_relative_pointer_listener, ss);
+ }
- if (ss->registry->wp_pointer_gestures) {
- ss->wp_pointer_gesture_pinch = zwp_pointer_gestures_v1_get_pinch_gesture(ss->registry->wp_pointer_gestures, ss->wl_pointer);
- zwp_pointer_gesture_pinch_v1_add_listener(ss->wp_pointer_gesture_pinch, &wp_pointer_gesture_pinch_listener, ss);
- }
+ if (ss->registry->wp_pointer_gestures) {
+ ss->wp_pointer_gesture_pinch = zwp_pointer_gestures_v1_get_pinch_gesture(ss->registry->wp_pointer_gestures, ss->wl_pointer);
+ zwp_pointer_gesture_pinch_v1_add_listener(ss->wp_pointer_gesture_pinch, &wp_pointer_gesture_pinch_listener, ss);
+ }
- // TODO: Constrain new pointers if the global mouse mode is constrained.
+ // TODO: Constrain new pointers if the global mouse mode is constrained.
+ }
} else {
if (ss->cursor_frame_callback) {
// Just in case. I got bitten by weird race-like conditions already.
@@ -1317,11 +1319,13 @@ void WaylandThread::_wl_seat_on_capabilities(void *data, struct wl_seat *wl_seat
// Keyboard handling.
if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) {
- ss->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
- ERR_FAIL_NULL(ss->xkb_context);
+ if (!ss->wl_keyboard) {
+ ss->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+ ERR_FAIL_NULL(ss->xkb_context);
- ss->wl_keyboard = wl_seat_get_keyboard(wl_seat);
- wl_keyboard_add_listener(ss->wl_keyboard, &wl_keyboard_listener, ss);
+ ss->wl_keyboard = wl_seat_get_keyboard(wl_seat);
+ wl_keyboard_add_listener(ss->wl_keyboard, &wl_keyboard_listener, ss);
+ }
} else {
if (ss->xkb_context) {
xkb_context_unref(ss->xkb_context);
@@ -1412,10 +1416,10 @@ void WaylandThread::_wl_pointer_on_motion(void *data, struct wl_pointer *wl_poin
PointerData &pd = ss->pointer_data_buffer;
// TODO: Scale only when sending the Wayland message.
- pd.position.x = wl_fixed_to_int(surface_x);
- pd.position.y = wl_fixed_to_int(surface_y);
+ pd.position.x = wl_fixed_to_double(surface_x);
+ pd.position.y = wl_fixed_to_double(surface_y);
- pd.position = scale_vector2i(pd.position, window_state_get_scale_factor(ws));
+ pd.position *= window_state_get_scale_factor(ws);
pd.motion_time = time;
}
@@ -1528,7 +1532,7 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point
mm->set_position(pd.position);
mm->set_global_position(pd.position);
- Vector2i pos_delta = pd.position - old_pd.position;
+ Vector2 pos_delta = pd.position - old_pd.position;
if (old_pd.relative_motion_time != pd.relative_motion_time) {
uint32_t time_delta = pd.relative_motion_time - old_pd.relative_motion_time;
@@ -1645,7 +1649,7 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point
// We have to set the last position pressed here as we can't take for
// granted what the individual events might have seen due to them not having
- // a garaunteed order.
+ // a guaranteed order.
if (mb->is_pressed()) {
pd.last_pressed_position = pd.position;
}
@@ -2047,11 +2051,21 @@ void WaylandThread::_wp_relative_pointer_on_relative_motion(void *data, struct z
SeatState *ss = (SeatState *)data;
ERR_FAIL_NULL(ss);
+ if (!ss->pointed_surface) {
+ // We're probably on a decoration or some other third-party thing.
+ return;
+ }
+
PointerData &pd = ss->pointer_data_buffer;
+ WindowState *ws = wl_surface_get_window_state(ss->pointed_surface);
+ ERR_FAIL_NULL(ws);
+
pd.relative_motion.x = wl_fixed_to_double(dx);
pd.relative_motion.y = wl_fixed_to_double(dy);
+ pd.relative_motion *= window_state_get_scale_factor(ws);
+
pd.relative_motion_time = uptime_lo;
}
@@ -2244,13 +2258,11 @@ void WaylandThread::_wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v
void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) {
TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
-
if (!ts) {
return;
}
SeatState *ss = wl_seat_get_seat_state(ts->wl_seat);
-
if (!ss) {
return;
}
@@ -2270,14 +2282,17 @@ void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_too
}
void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface) {
- TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
+ if (!surface || !wl_proxy_is_godot((struct wl_proxy *)surface)) {
+ // We're probably on a decoration or something.
+ return;
+ }
+ TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
if (!ts) {
return;
}
SeatState *ss = wl_seat_get_seat_state(ts->wl_seat);
-
if (!ss) {
return;
}
@@ -2299,13 +2314,12 @@ void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_table
void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) {
TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
-
- if (!ts) {
+ if (!ts || !ts->data_pending.proximal_surface) {
+ // Not our stuff, we don't care.
return;
}
SeatState *ss = wl_seat_get_seat_state(ts->wl_seat);
-
if (!ss) {
return;
}
@@ -2326,7 +2340,6 @@ void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tabl
void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial) {
TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
-
if (!ts) {
return;
}
@@ -2344,7 +2357,6 @@ void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v
void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) {
TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
-
if (!ts) {
return;
}
@@ -2360,11 +2372,15 @@ void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2
void WaylandThread::_wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y) {
TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
-
if (!ts) {
return;
}
+ if (!ts->data_pending.proximal_surface) {
+ // We're probably on a decoration or some other third-party thing.
+ return;
+ }
+
WindowState *ws = wl_surface_get_window_state(ts->data_pending.proximal_surface);
ERR_FAIL_NULL(ws);
@@ -2372,16 +2388,15 @@ void WaylandThread::_wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool
double scale_factor = window_state_get_scale_factor(ws);
- td.position.x = wl_fixed_to_int(x);
- td.position.y = wl_fixed_to_int(y);
- td.position = scale_vector2i(td.position, scale_factor);
+ td.position.x = wl_fixed_to_double(x);
+ td.position.y = wl_fixed_to_double(y);
+ td.position *= scale_factor;
td.motion_time = OS::get_singleton()->get_ticks_msec();
}
void WaylandThread::_wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t pressure) {
TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
-
if (!ts) {
return;
}
@@ -2395,7 +2410,6 @@ void WaylandThread::_wp_tablet_tool_on_distance(void *data, struct zwp_tablet_to
void WaylandThread::_wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y) {
TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
-
if (!ts) {
return;
}
@@ -2420,7 +2434,6 @@ void WaylandThread::_wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_
void WaylandThread::_wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, uint32_t button, uint32_t state) {
TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
-
if (!ts) {
return;
}
@@ -2456,13 +2469,11 @@ void WaylandThread::_wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool
void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t time) {
TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2);
-
if (!ts) {
return;
}
SeatState *ss = wl_seat_get_seat_state(ts->wl_seat);
-
if (!ss) {
return;
}
@@ -2509,7 +2520,7 @@ void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_
mm->set_relative(td.position - old_td.position);
mm->set_relative_screen_position(mm->get_relative());
- Vector2i pos_delta = td.position - old_td.position;
+ Vector2 pos_delta = td.position - old_td.position;
uint32_t time_delta = td.motion_time - old_td.motion_time;
mm->set_velocity((Vector2)pos_delta / time_delta);
@@ -3240,6 +3251,8 @@ void WaylandThread::window_create(DisplayServer::WindowID p_window_id, int p_wid
zxdg_exported_v1_add_listener(ws.xdg_exported, &xdg_exported_listener, &ws);
}
+ wl_surface_commit(ws.wl_surface);
+
// Wait for the surface to be configured before continuing.
wl_display_roundtrip(wl_display);
}
diff --git a/platform/linuxbsd/wayland/wayland_thread.h b/platform/linuxbsd/wayland/wayland_thread.h
index 775ca71346..84e9bdc2dc 100644
--- a/platform/linuxbsd/wayland/wayland_thread.h
+++ b/platform/linuxbsd/wayland/wayland_thread.h
@@ -44,7 +44,7 @@
#include <wayland-client-core.h>
#include <wayland-cursor.h>
#ifdef GLES3_ENABLED
-#include <wayland-egl.h>
+#include <wayland-egl-core.h>
#endif
#include <xkbcommon/xkbcommon.h>
#endif // SOWRAP_ENABLED
@@ -295,7 +295,7 @@ public:
};
struct PointerData {
- Point2i position;
+ Point2 position;
uint32_t motion_time = 0;
// Relative motion has its own optional event and so needs its own time.
@@ -305,7 +305,7 @@ public:
BitField<MouseButtonMask> pressed_button_mask;
MouseButton last_button_pressed = MouseButton::NONE;
- Point2i last_pressed_position;
+ Point2 last_pressed_position;
// This is needed to check for a new double click every time.
bool double_click_begun = false;
@@ -325,14 +325,14 @@ public:
};
struct TabletToolData {
- Point2i position;
+ Point2 position;
Vector2 tilt;
uint32_t pressure = 0;
BitField<MouseButtonMask> pressed_button_mask;
MouseButton last_button_pressed = MouseButton::NONE;
- Point2i last_pressed_position;
+ Point2 last_pressed_position;
bool double_click_begun = false;
diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp
index edf3a40ccb..8a2f83be2d 100644
--- a/platform/linuxbsd/x11/display_server_x11.cpp
+++ b/platform/linuxbsd/x11/display_server_x11.cpp
@@ -1519,7 +1519,7 @@ Color DisplayServerX11::screen_get_pixel(const Point2i &p_position) const {
if (image) {
XColor c;
c.pixel = XGetPixel(image, 0, 0);
- XFree(image);
+ XDestroyImage(image);
XQueryColor(x11_display, XDefaultColormap(x11_display, i), &c);
color = Color(float(c.red) / 65535.0, float(c.green) / 65535.0, float(c.blue) / 65535.0, 1.0);
break;
@@ -1637,11 +1637,12 @@ Ref<Image> DisplayServerX11::screen_get_image(int p_screen) const {
}
}
} else {
- XFree(image);
- ERR_FAIL_V_MSG(Ref<Image>(), vformat("XImage with RGB mask %x %x %x and depth %d is not supported.", (uint64_t)image->red_mask, (uint64_t)image->green_mask, (uint64_t)image->blue_mask, (int64_t)image->bits_per_pixel));
+ String msg = vformat("XImage with RGB mask %x %x %x and depth %d is not supported.", (uint64_t)image->red_mask, (uint64_t)image->green_mask, (uint64_t)image->blue_mask, (int64_t)image->bits_per_pixel);
+ XDestroyImage(image);
+ ERR_FAIL_V_MSG(Ref<Image>(), msg);
}
img = Image::create_from_data(width, height, false, Image::FORMAT_RGBA8, img_data);
- XFree(image);
+ XDestroyImage(image);
}
return img;
@@ -1734,7 +1735,7 @@ Vector<DisplayServer::WindowID> DisplayServerX11::get_window_list() const {
return ret;
}
-DisplayServer::WindowID DisplayServerX11::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect) {
+DisplayServer::WindowID DisplayServerX11::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect, bool p_exclusive, WindowID p_transient_parent) {
_THREAD_SAFE_METHOD_
WindowID id = _create_window(p_mode, p_vsync_mode, p_flags, p_rect);
@@ -1748,6 +1749,11 @@ DisplayServer::WindowID DisplayServerX11::create_sub_window(WindowMode p_mode, V
rendering_device->screen_create(id);
}
#endif
+
+ if (p_transient_parent != INVALID_WINDOW_ID) {
+ window_set_transient(id, p_transient_parent);
+ }
+
return id;
}
@@ -4964,6 +4970,23 @@ void DisplayServerX11::process_events() {
pos = Point2i(windows[focused_window_id].size.width / 2, windows[focused_window_id].size.height / 2);
}
+ BitField<MouseButtonMask> last_button_state = 0;
+ if (event.xmotion.state & Button1Mask) {
+ last_button_state.set_flag(MouseButtonMask::LEFT);
+ }
+ if (event.xmotion.state & Button2Mask) {
+ last_button_state.set_flag(MouseButtonMask::MIDDLE);
+ }
+ if (event.xmotion.state & Button3Mask) {
+ last_button_state.set_flag(MouseButtonMask::RIGHT);
+ }
+ if (event.xmotion.state & Button4Mask) {
+ last_button_state.set_flag(MouseButtonMask::MB_XBUTTON1);
+ }
+ if (event.xmotion.state & Button5Mask) {
+ last_button_state.set_flag(MouseButtonMask::MB_XBUTTON2);
+ }
+
Ref<InputEventMouseMotion> mm;
mm.instantiate();
@@ -4971,13 +4994,13 @@ void DisplayServerX11::process_events() {
if (xi.pressure_supported) {
mm->set_pressure(xi.pressure);
} else {
- mm->set_pressure(bool(mouse_get_button_state().has_flag(MouseButtonMask::LEFT)) ? 1.0f : 0.0f);
+ mm->set_pressure(bool(last_button_state.has_flag(MouseButtonMask::LEFT)) ? 1.0f : 0.0f);
}
mm->set_tilt(xi.tilt);
mm->set_pen_inverted(xi.pen_inverted);
_get_key_modifier_state(event.xmotion.state, mm);
- mm->set_button_mask(mouse_get_button_state());
+ mm->set_button_mask(last_button_state);
mm->set_position(pos);
mm->set_global_position(pos);
mm->set_velocity(Input::get_singleton()->get_last_mouse_velocity());
diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h
index 341ba5f079..0cbfbe51ef 100644
--- a/platform/linuxbsd/x11/display_server_x11.h
+++ b/platform/linuxbsd/x11/display_server_x11.h
@@ -438,7 +438,7 @@ public:
virtual Vector<DisplayServer::WindowID> get_window_list() const override;
- virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()) override;
+ virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i(), bool p_exclusive = false, WindowID p_transient_parent = INVALID_WINDOW_ID) override;
virtual void show_window(WindowID p_id) override;
virtual void delete_sub_window(WindowID p_id) override;
diff --git a/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp b/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp
index bf44062266..3f505d000c 100644
--- a/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp
+++ b/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp
@@ -51,7 +51,7 @@ RenderingContextDriver::SurfaceID RenderingContextDriverVulkanX11::surface_creat
create_info.window = wpd->window;
VkSurfaceKHR vk_surface = VK_NULL_HANDLE;
- VkResult err = vkCreateXlibSurfaceKHR(instance_get(), &create_info, nullptr, &vk_surface);
+ VkResult err = vkCreateXlibSurfaceKHR(instance_get(), &create_info, get_allocation_callbacks(VK_OBJECT_TYPE_SURFACE_KHR), &vk_surface);
ERR_FAIL_COND_V(err != VK_SUCCESS, SurfaceID());
Surface *surface = memnew(Surface);
diff --git a/platform/macos/SCsub b/platform/macos/SCsub
index c965e875c1..a10262c524 100644
--- a/platform/macos/SCsub
+++ b/platform/macos/SCsub
@@ -23,10 +23,10 @@ def generate_bundle(target, source, env):
prefix += ".double"
# Lipo editor executable.
- target_bin = lipo(bin_dir + "/" + prefix, env.extra_suffix)
+ target_bin = lipo(bin_dir + "/" + prefix, env.extra_suffix + env.module_version_string)
# Assemble .app bundle and update version info.
- app_dir = Dir("#bin/" + (prefix + env.extra_suffix).replace(".", "_") + ".app").abspath
+ app_dir = Dir("#bin/" + (prefix + env.extra_suffix + env.module_version_string).replace(".", "_") + ".app").abspath
templ = Dir("#misc/dist/macos_tools.app").abspath
if os.path.exists(app_dir):
shutil.rmtree(app_dir)
@@ -35,6 +35,8 @@ def generate_bundle(target, source, env):
os.mkdir(app_dir + "/Contents/MacOS")
if target_bin != "":
shutil.copy(target_bin, app_dir + "/Contents/MacOS/Godot")
+ if "mono" in env.module_version_string:
+ shutil.copytree(Dir("#bin/GodotSharp").abspath, app_dir + "/Contents/Resources/GodotSharp")
version = get_build_version(False)
short_version = get_build_version(True)
with open(Dir("#misc/dist/macos").abspath + "/editor_info_plist.template", "rt", encoding="utf-8") as fin:
@@ -76,8 +78,8 @@ def generate_bundle(target, source, env):
dbg_prefix += ".double"
# Lipo template executables.
- rel_target_bin = lipo(bin_dir + "/" + rel_prefix, env.extra_suffix)
- dbg_target_bin = lipo(bin_dir + "/" + dbg_prefix, env.extra_suffix)
+ rel_target_bin = lipo(bin_dir + "/" + rel_prefix, env.extra_suffix + env.module_version_string)
+ dbg_target_bin = lipo(bin_dir + "/" + dbg_prefix, env.extra_suffix + env.module_version_string)
# Assemble .app bundle.
app_dir = Dir("#bin/macos_template.app").abspath
@@ -93,7 +95,7 @@ def generate_bundle(target, source, env):
shutil.copy(dbg_target_bin, app_dir + "/Contents/MacOS/godot_macos_debug.universal")
# ZIP .app bundle.
- zip_dir = Dir("#bin/" + (app_prefix + env.extra_suffix).replace(".", "_")).abspath
+ zip_dir = Dir("#bin/" + (app_prefix + env.extra_suffix + env.module_version_string).replace(".", "_")).abspath
shutil.make_archive(zip_dir, "zip", root_dir=bin_dir, base_dir="macos_template.app")
shutil.rmtree(app_dir)
diff --git a/platform/macos/detect.py b/platform/macos/detect.py
index 70cb00c6ff..e35423d41f 100644
--- a/platform/macos/detect.py
+++ b/platform/macos/detect.py
@@ -56,7 +56,8 @@ def get_flags():
return {
"arch": detect_arch(),
"use_volk": False,
- "supported": ["mono"],
+ "metal": True,
+ "supported": ["metal", "mono"],
}
@@ -96,6 +97,8 @@ def configure(env: "SConsEnvironment"):
env.Append(CCFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.13"])
env.Append(LINKFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.13"])
+ env.Append(CCFLAGS=["-ffp-contract=off"])
+
cc_version = get_compiler_version(env)
cc_version_major = cc_version["apple_major"]
cc_version_minor = cc_version["apple_minor"]
@@ -237,9 +240,22 @@ def configure(env: "SConsEnvironment"):
env.Append(LINKFLAGS=["-rpath", "@executable_path/../Frameworks", "-rpath", "@executable_path"])
+ if env["metal"] and env["arch"] != "arm64":
+ # Only supported on arm64, so skip it for x86_64 builds.
+ env["metal"] = False
+
+ extra_frameworks = set()
+
+ if env["metal"]:
+ env.AppendUnique(CPPDEFINES=["METAL_ENABLED", "RD_ENABLED"])
+ extra_frameworks.add("Metal")
+ extra_frameworks.add("MetalKit")
+ env.Prepend(CPPPATH=["#thirdparty/spirv-cross"])
+
if env["vulkan"]:
- env.Append(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"])
- env.Append(LINKFLAGS=["-framework", "Metal", "-framework", "IOSurface"])
+ env.AppendUnique(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"])
+ extra_frameworks.add("Metal")
+ extra_frameworks.add("IOSurface")
if not env["use_volk"]:
env.Append(LINKFLAGS=["-lMoltenVK"])
@@ -258,3 +274,7 @@ def configure(env: "SConsEnvironment"):
"MoltenVK SDK installation directory not found, use 'vulkan_sdk_path' SCons parameter to specify SDK path."
)
sys.exit(255)
+
+ if len(extra_frameworks) > 0:
+ frameworks = [item for key in extra_frameworks for item in ["-framework", key]]
+ env.Append(LINKFLAGS=frameworks)
diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h
index b4741dc08f..97af6d0a5a 100644
--- a/platform/macos/display_server_macos.h
+++ b/platform/macos/display_server_macos.h
@@ -47,6 +47,9 @@
#if defined(VULKAN_ENABLED)
#include "rendering_context_driver_vulkan_macos.h"
#endif // VULKAN_ENABLED
+#if defined(METAL_ENABLED)
+#include "drivers/metal/rendering_context_driver_metal.h"
+#endif
#endif // RD_ENABLED
#define BitMap _QDBitMap // Suppress deprecated QuickDraw definition.
@@ -326,7 +329,7 @@ public:
virtual Vector<int> get_window_list() const override;
- virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()) override;
+ virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i(), bool p_exclusive = false, WindowID p_transient_parent = INVALID_WINDOW_ID) override;
virtual void show_window(WindowID p_id) override;
virtual void delete_sub_window(WindowID p_id) override;
diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm
index a1a91345ac..989a9dcf6c 100644
--- a/platform/macos/display_server_macos.mm
+++ b/platform/macos/display_server_macos.mm
@@ -139,12 +139,20 @@ DisplayServerMacOS::WindowID DisplayServerMacOS::_create_window(WindowMode p_mod
#ifdef VULKAN_ENABLED
RenderingContextDriverVulkanMacOS::WindowPlatformData vulkan;
#endif
+#ifdef METAL_ENABLED
+ RenderingContextDriverMetal::WindowPlatformData metal;
+#endif
} wpd;
#ifdef VULKAN_ENABLED
if (rendering_driver == "vulkan") {
wpd.vulkan.layer_ptr = (CAMetalLayer *const *)&layer;
}
#endif
+#ifdef METAL_ENABLED
+ if (rendering_driver == "metal") {
+ wpd.metal.layer = (CAMetalLayer *)layer;
+ }
+#endif
Error err = rendering_context->window_create(window_id_counter, &wpd);
ERR_FAIL_COND_V_MSG(err != OK, INVALID_WINDOW_ID, vformat("Can't create a %s context", rendering_driver));
@@ -568,23 +576,7 @@ void DisplayServerMacOS::menu_callback(id p_sender) {
}
GodotMenuItem *value = [p_sender representedObject];
-
if (value) {
- if (value->max_states > 0) {
- value->state++;
- if (value->state >= value->max_states) {
- value->state = 0;
- }
- }
-
- if (value->checkable_type == CHECKABLE_TYPE_CHECK_BOX) {
- if ([p_sender state] == NSControlStateValueOff) {
- [p_sender setState:NSControlStateValueOn];
- } else {
- [p_sender setState:NSControlStateValueOff];
- }
- }
-
if (value->callback.is_valid()) {
MenuCall mc;
mc.tag = value->meta;
@@ -1730,7 +1722,7 @@ Vector<DisplayServer::WindowID> DisplayServerMacOS::get_window_list() const {
return ret;
}
-DisplayServer::WindowID DisplayServerMacOS::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect) {
+DisplayServer::WindowID DisplayServerMacOS::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect, bool p_exclusive, WindowID p_transient_parent) {
_THREAD_SAFE_METHOD_
WindowID id = _create_window(p_mode, p_vsync_mode, p_rect);
@@ -1744,6 +1736,12 @@ DisplayServer::WindowID DisplayServerMacOS::create_sub_window(WindowMode p_mode,
rendering_device->screen_create(id);
}
#endif
+
+ window_set_exclusive(id, p_exclusive);
+ if (p_transient_parent != INVALID_WINDOW_ID) {
+ window_set_transient(id, p_transient_parent);
+ }
+
return id;
}
@@ -2342,7 +2340,7 @@ void DisplayServerMacOS::window_set_window_buttons_offset(const Vector2i &p_offs
wd.wb_offset = p_offset / scale;
wd.wb_offset = wd.wb_offset.maxi(12);
if (wd.window_button_view) {
- [wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)];
+ [(GodotButtonView *)wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)];
}
}
@@ -2710,7 +2708,7 @@ void DisplayServerMacOS::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_
gl_manager_legacy->set_use_vsync(p_vsync_mode != DisplayServer::VSYNC_DISABLED);
}
#endif
-#if defined(VULKAN_ENABLED)
+#if defined(RD_ENABLED)
if (rendering_context) {
rendering_context->window_set_vsync_mode(p_window, p_vsync_mode);
}
@@ -2727,7 +2725,7 @@ DisplayServer::VSyncMode DisplayServerMacOS::window_get_vsync_mode(WindowID p_wi
return (gl_manager_legacy->is_using_vsync() ? DisplayServer::VSyncMode::VSYNC_ENABLED : DisplayServer::VSyncMode::VSYNC_DISABLED);
}
#endif
-#if defined(VULKAN_ENABLED)
+#if defined(RD_ENABLED)
if (rendering_context) {
return rendering_context->window_get_vsync_mode(p_window);
}
@@ -3311,6 +3309,9 @@ Vector<String> DisplayServerMacOS::get_rendering_drivers_func() {
#if defined(VULKAN_ENABLED)
drivers.push_back("vulkan");
#endif
+#if defined(METAL_ENABLED)
+ drivers.push_back("metal");
+#endif
#if defined(GLES3_ENABLED)
drivers.push_back("opengl3");
drivers.push_back("opengl3_angle");
@@ -3608,7 +3609,11 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM
gl_manager_angle = nullptr;
bool fallback = GLOBAL_GET("rendering/gl_compatibility/fallback_to_native");
if (fallback) {
- WARN_PRINT("Your video card drivers seem not to support the required Metal version, switching to native OpenGL.");
+#ifdef EGL_STATIC
+ WARN_PRINT("Your video card drivers seem not to support GLES3 / ANGLE, switching to native OpenGL.");
+#else
+ WARN_PRINT("Your video card drivers seem not to support GLES3 / ANGLE or ANGLE dynamic libraries (libEGL.dylib and libGLESv2.dylib) are missing, switching to native OpenGL.");
+#endif
rendering_driver = "opengl3";
} else {
r_error = ERR_UNAVAILABLE;
@@ -3633,6 +3638,11 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM
rendering_context = memnew(RenderingContextDriverVulkanMacOS);
}
#endif
+#if defined(METAL_ENABLED)
+ if (rendering_driver == "metal") {
+ rendering_context = memnew(RenderingContextDriverMetal);
+ }
+#endif
if (rendering_context) {
if (rendering_context->initialize() != OK) {
diff --git a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml
index 92ade4b77a..34ad52bbf6 100644
--- a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml
+++ b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml
@@ -224,16 +224,16 @@
Indicates whether your app uses advertising data for tracking.
</member>
<member name="privacy/collected_data/audio_data/collected" type="bool" setter="" getter="">
- Indicates whether your app collects audio data data.
+ Indicates whether your app collects audio data.
</member>
<member name="privacy/collected_data/audio_data/collection_purposes" type="int" setter="" getter="">
The reasons your app collects audio data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url].
</member>
<member name="privacy/collected_data/audio_data/linked_to_user" type="bool" setter="" getter="">
- Indicates whether your app links audio data data to the user's identity.
+ Indicates whether your app links audio data to the user's identity.
</member>
<member name="privacy/collected_data/audio_data/used_for_tracking" type="bool" setter="" getter="">
- Indicates whether your app uses audio data data for tracking.
+ Indicates whether your app uses audio data for tracking.
</member>
<member name="privacy/collected_data/browsing_history/collected" type="bool" setter="" getter="">
Indicates whether your app collects browsing history.
diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp
index 73e2f2d45b..290b0082fc 100644
--- a/platform/macos/export/export_plugin.cpp
+++ b/platform/macos/export/export_plugin.cpp
@@ -141,7 +141,7 @@ String EditorExportPlatformMacOS::get_export_option_warning(const EditorExportPr
if (p_name == "codesign/codesign") {
if (dist_type == 2) {
- if (codesign_tool == 2 && Engine::get_singleton()->has_singleton("GodotSharp")) {
+ if (codesign_tool == 2 && ClassDB::class_exists("CSharpScript")) {
return TTR("'rcodesign' doesn't support signing applications with embedded dynamic libraries (GDExtension or .NET).");
}
if (codesign_tool == 0) {
@@ -333,7 +333,7 @@ bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportP
}
// These entitlements are required to run managed code, and are always enabled in Mono builds.
- if (Engine::get_singleton()->has_singleton("GodotSharp")) {
+ if (ClassDB::class_exists("CSharpScript")) {
if (p_option == "codesign/entitlements/allow_jit_code_execution" || p_option == "codesign/entitlements/allow_unsigned_executable_memory" || p_option == "codesign/entitlements/allow_dyld_environment_variables") {
return false;
}
@@ -458,6 +458,7 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/additional_plist_content", PROPERTY_HINT_MULTILINE_TEXT), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/platform_build"), "14C18"));
+ // TODO(sgc): Need to set appropriate version when using Metal
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_version"), "13.1"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_build"), "22C55"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_name"), "macosx13.1"));
@@ -1064,7 +1065,7 @@ Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_pres
return OK;
}
-Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn, bool p_set_id) {
+void EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn, bool p_set_id) {
int codesign_tool = p_preset->get("codesign/codesign");
switch (codesign_tool) {
case 1: { // built-in ad-hoc
@@ -1074,7 +1075,7 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre
Error err = CodeSign::codesign(false, true, p_path, p_ent_path, error_msg);
if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Built-in CodeSign failed with error \"%s\"."), error_msg));
- return Error::FAILED;
+ return;
}
#else
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Built-in CodeSign require regex module."));
@@ -1086,13 +1087,13 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre
String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String();
if (rcodesign.is_empty()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xrcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign)."));
- return Error::FAILED;
+ return;
}
List<String> args;
args.push_back("sign");
- if (p_path.get_extension() != "dmg") {
+ if (!p_ent_path.is_empty()) {
args.push_back("--entitlements-xml-path");
args.push_back(p_ent_path);
}
@@ -1124,13 +1125,13 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre
Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true);
if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start rcodesign executable."));
- return err;
+ return;
}
if (exitcode != 0) {
print_line("rcodesign (" + p_path + "):\n" + str);
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details."));
- return Error::FAILED;
+ return;
} else {
print_verbose("rcodesign (" + p_path + "):\n" + str);
}
@@ -1141,7 +1142,7 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre
if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xcode command line tools are not installed."));
- return Error::FAILED;
+ return;
}
bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");
@@ -1153,7 +1154,7 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre
args.push_back("runtime");
}
- if (p_path.get_extension() != "dmg") {
+ if (!p_ent_path.is_empty()) {
args.push_back("--entitlements");
args.push_back(p_ent_path);
}
@@ -1190,13 +1191,13 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre
Error err = OS::get_singleton()->execute("codesign", args, &str, &exitcode, true);
if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start codesign executable, make sure Xcode command line tools are installed."));
- return err;
+ return;
}
if (exitcode != 0) {
print_line("codesign (" + p_path + "):\n" + str);
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details."));
- return Error::FAILED;
+ return;
} else {
print_verbose("codesign (" + p_path + "):\n" + str);
}
@@ -1205,14 +1206,13 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre
default: {
};
}
-
- return OK;
}
-Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path,
+void EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path,
const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code) {
static Vector<String> extensions_to_sign;
+ bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled");
if (extensions_to_sign.is_empty()) {
extensions_to_sign.push_back("dylib");
extensions_to_sign.push_back("framework");
@@ -1223,7 +1223,8 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPres
Ref<DirAccess> dir_access{ DirAccess::open(p_path, &dir_access_error) };
if (dir_access_error != OK) {
- return dir_access_error;
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign directory %s."), p_path));
+ return;
}
dir_access->list_dir_begin();
@@ -1237,44 +1238,35 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPres
}
if (extensions_to_sign.has(current_file.get_extension())) {
- String ent_path = p_ent_path;
+ String ent_path;
bool set_bundle_id = false;
- if (FileAccess::exists(current_file_path)) {
+ if (sandbox && FileAccess::exists(current_file_path)) {
int ftype = MachO::get_filetype(current_file_path);
if (ftype == 2 || ftype == 5) {
ent_path = p_helper_ent_path;
set_bundle_id = true;
}
}
- Error code_sign_error{ _code_sign(p_preset, current_file_path, ent_path, false, set_bundle_id) };
- if (code_sign_error != OK) {
- return code_sign_error;
- }
+ _code_sign(p_preset, current_file_path, ent_path, false, set_bundle_id);
if (is_executable(current_file_path)) {
// chmod with 0755 if the file is executable.
FileAccess::set_unix_permissions(current_file_path, 0755);
}
} else if (dir_access->current_is_dir()) {
- Error code_sign_error{ _code_sign_directory(p_preset, current_file_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code) };
- if (code_sign_error != OK) {
- return code_sign_error;
- }
+ _code_sign_directory(p_preset, current_file_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code);
} else if (p_should_error_on_non_code) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign file %s."), current_file));
- return Error::FAILED;
}
current_file = dir_access->get_next();
}
-
- return OK;
}
Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path,
const String &p_in_app_path, bool p_sign_enabled,
const Ref<EditorExportPreset> &p_preset, const String &p_ent_path,
const String &p_helper_ent_path,
- bool p_should_error_on_non_code_sign) {
+ bool p_should_error_on_non_code_sign, bool p_sandbox) {
static Vector<String> extensions_to_sign;
if (extensions_to_sign.is_empty()) {
@@ -1363,19 +1355,19 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access
if (err == OK && p_sign_enabled) {
if (dir_access->dir_exists(p_src_path) && p_src_path.get_extension().is_empty()) {
// If it is a directory, find and sign all dynamic libraries.
- err = _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code_sign);
+ _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code_sign);
} else {
if (extensions_to_sign.has(p_in_app_path.get_extension())) {
- String ent_path = p_ent_path;
+ String ent_path;
bool set_bundle_id = false;
- if (FileAccess::exists(p_in_app_path)) {
+ if (p_sandbox && FileAccess::exists(p_in_app_path)) {
int ftype = MachO::get_filetype(p_in_app_path);
if (ftype == 2 || ftype == 5) {
ent_path = p_helper_ent_path;
set_bundle_id = true;
}
}
- err = _code_sign(p_preset, p_in_app_path, ent_path, false, set_bundle_id);
+ _code_sign(p_preset, p_in_app_path, ent_path, false, set_bundle_id);
}
if (dir_access->file_exists(p_in_app_path) && is_executable(p_in_app_path)) {
// chmod with 0755 if the file is executable.
@@ -1389,13 +1381,13 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access
Error EditorExportPlatformMacOS::_export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin,
const String &p_app_path_name, Ref<DirAccess> &dir_access,
bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset,
- const String &p_ent_path, const String &p_helper_ent_path) {
+ const String &p_ent_path, const String &p_helper_ent_path, bool p_sandbox) {
Error error{ OK };
const Vector<String> &macos_plugins{ p_editor_export_plugin->get_macos_plugin_files() };
for (int i = 0; i < macos_plugins.size(); ++i) {
String src_path{ ProjectSettings::get_singleton()->globalize_path(macos_plugins[i]) };
String path_in_app{ p_app_path_name + "/Contents/PlugIns/" + src_path.get_file() };
- error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, p_helper_ent_path, false);
+ error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, p_helper_ent_path, false, p_sandbox);
if (error != OK) {
break;
}
@@ -1988,7 +1980,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
ent_f->store_line("<plist version=\"1.0\">");
ent_f->store_line("<dict>");
- if (Engine::get_singleton()->has_singleton("GodotSharp")) {
+ if (ClassDB::class_exists("CSharpScript")) {
// These entitlements are required to run managed code, and are always enabled in Mono builds.
ent_f->store_line("<key>com.apple.security.cs.allow-jit</key>");
ent_f->store_line("<true/>");
@@ -2156,7 +2148,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
String hlp_path = helpers[i];
err = da->copy(hlp_path, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file());
if (err == OK && sign_enabled) {
- err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false, true);
+ _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false, true);
}
FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755);
}
@@ -2168,11 +2160,11 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path);
if (shared_objects[i].target.is_empty()) {
String path_in_app = tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file();
- err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, hlp_ent_path, true);
+ err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, hlp_ent_path, true, sandbox);
} else {
String path_in_app = tmp_app_path_name.path_join(shared_objects[i].target);
tmp_app_dir->make_dir_recursive(path_in_app);
- err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, hlp_ent_path, false);
+ err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, hlp_ent_path, false, sandbox);
}
if (err != OK) {
break;
@@ -2181,7 +2173,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
Vector<Ref<EditorExportPlugin>> export_plugins{ EditorExport::get_singleton()->get_export_plugins() };
for (int i = 0; i < export_plugins.size(); ++i) {
- err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path, hlp_ent_path);
+ err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path, hlp_ent_path, sandbox);
if (err != OK) {
break;
}
@@ -2201,7 +2193,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
if (ep.step(TTR("Code signing bundle"), 2)) {
return ERR_SKIP;
}
- err = _code_sign(p_preset, tmp_app_path_name, ent_path, true, false);
+ _code_sign(p_preset, tmp_app_path_name, ent_path, true, false);
}
String noto_path = p_path;
@@ -2219,7 +2211,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
if (ep.step(TTR("Code signing DMG"), 3)) {
return ERR_SKIP;
}
- err = _code_sign(p_preset, p_path, ent_path, false, false);
+ _code_sign(p_preset, p_path, ent_path, false, false);
}
} else if (export_format == "pkg") {
// Create a Installer.
diff --git a/platform/macos/export/export_plugin.h b/platform/macos/export/export_plugin.h
index 6134d756b9..062a2e5f95 100644
--- a/platform/macos/export/export_plugin.h
+++ b/platform/macos/export/export_plugin.h
@@ -90,14 +90,14 @@ class EditorExportPlatformMacOS : public EditorExportPlatform {
void _make_icon(const Ref<EditorExportPreset> &p_preset, const Ref<Image> &p_icon, Vector<uint8_t> &p_data);
Error _notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path);
- Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true, bool p_set_id = false);
- Error _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code = true);
+ void _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true, bool p_set_id = false);
+ void _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code = true);
Error _copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path,
bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, const String &p_helper_ent_path,
- bool p_should_error_on_non_code_sign);
+ bool p_should_error_on_non_code_sign, bool p_sandbox);
Error _export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, const String &p_app_path_name,
Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset,
- const String &p_ent_path, const String &p_helper_ent_path);
+ const String &p_ent_path, const String &p_helper_ent_path, bool p_sandbox);
Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name);
Error _create_pkg(const Ref<EditorExportPreset> &p_preset, const String &p_pkg_path, const String &p_app_path_name);
Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path);
diff --git a/platform/macos/gl_manager_macos_legacy.h b/platform/macos/gl_manager_macos_legacy.h
index af9be8f5ba..383c5c3306 100644
--- a/platform/macos/gl_manager_macos_legacy.h
+++ b/platform/macos/gl_manager_macos_legacy.h
@@ -62,6 +62,7 @@ class GLManagerLegacy_MacOS {
Error create_context(GLWindow &win);
+ bool framework_loaded = false;
bool use_vsync = false;
CGLEnablePtr CGLEnable = nullptr;
CGLSetParameterPtr CGLSetParameter = nullptr;
diff --git a/platform/macos/gl_manager_macos_legacy.mm b/platform/macos/gl_manager_macos_legacy.mm
index 6ce3831d9c..a0d037144e 100644
--- a/platform/macos/gl_manager_macos_legacy.mm
+++ b/platform/macos/gl_manager_macos_legacy.mm
@@ -32,6 +32,7 @@
#if defined(MACOS_ENABLED) && defined(GLES3_ENABLED)
+#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
@@ -156,7 +157,7 @@ void GLManagerLegacy_MacOS::window_set_per_pixel_transparency_enabled(DisplaySer
}
Error GLManagerLegacy_MacOS::initialize() {
- return OK;
+ return framework_loaded ? OK : ERR_CANT_CREATE;
}
void GLManagerLegacy_MacOS::set_use_vsync(bool p_use) {
@@ -186,12 +187,17 @@ NSOpenGLContext *GLManagerLegacy_MacOS::get_context(DisplayServer::WindowID p_wi
}
GLManagerLegacy_MacOS::GLManagerLegacy_MacOS() {
- CFBundleRef framework = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.opengl"));
- CFBundleLoadExecutable(framework);
-
- CGLEnable = (CGLEnablePtr)CFBundleGetFunctionPointerForName(framework, CFSTR("CGLEnable"));
- CGLSetParameter = (CGLSetParameterPtr)CFBundleGetFunctionPointerForName(framework, CFSTR("CGLSetParameter"));
- CGLGetCurrentContext = (CGLGetCurrentContextPtr)CFBundleGetFunctionPointerForName(framework, CFSTR("CGLGetCurrentContext"));
+ NSBundle *framework = [NSBundle bundleWithPath:@"/System/Library/Frameworks/OpenGL.framework"];
+ if (framework) {
+ void *library_handle = dlopen([framework.executablePath UTF8String], RTLD_NOW);
+ if (library_handle) {
+ CGLEnable = (CGLEnablePtr)dlsym(library_handle, "CGLEnable");
+ CGLSetParameter = (CGLSetParameterPtr)dlsym(library_handle, "CGLSetParameter");
+ CGLGetCurrentContext = (CGLGetCurrentContextPtr)dlsym(library_handle, "CGLGetCurrentContext");
+
+ framework_loaded = CGLEnable && CGLSetParameter && CGLGetCurrentContext;
+ }
+ }
}
GLManagerLegacy_MacOS::~GLManagerLegacy_MacOS() {
diff --git a/platform/macos/godot_content_view.mm b/platform/macos/godot_content_view.mm
index 77f3a28ae7..7d43ac9fe6 100644
--- a/platform/macos/godot_content_view.mm
+++ b/platform/macos/godot_content_view.mm
@@ -329,8 +329,9 @@
Callable::CallError ce;
wd.drop_files_callback.callp((const Variant **)&v_args, 1, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
- ERR_PRINT(vformat("Failed to execute drop files callback: %s.", Variant::get_callable_error_text(wd.drop_files_callback, v_args, 1, ce)));
+ ERR_FAIL_V_MSG(NO, vformat("Failed to execute drop files callback: %s.", Variant::get_callable_error_text(wd.drop_files_callback, v_args, 1, ce)));
}
+ return YES;
}
return NO;
diff --git a/platform/macos/godot_main_macos.mm b/platform/macos/godot_main_macos.mm
index 942c351ac0..eebaed0eaf 100644
--- a/platform/macos/godot_main_macos.mm
+++ b/platform/macos/godot_main_macos.mm
@@ -41,8 +41,8 @@
int main(int argc, char **argv) {
#if defined(VULKAN_ENABLED)
- // MoltenVK - enable full component swizzling support.
- setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1);
+ setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); // MoltenVK - enable full component swizzling support.
+ setenv("MVK_CONFIG_SWAPCHAIN_MIN_MAG_FILTER_USE_NEAREST", "0", 1); // MoltenVK - use linear surface scaling. TODO: remove when full DPI scaling is implemented.
#endif
#if defined(SANITIZERS_ENABLED)
diff --git a/platform/macos/godot_menu_item.h b/platform/macos/godot_menu_item.h
index b6e2d41c08..e1af317259 100644
--- a/platform/macos/godot_menu_item.h
+++ b/platform/macos/godot_menu_item.h
@@ -52,6 +52,7 @@ enum GlobalMenuCheckType {
Callable hover_callback;
Variant meta;
GlobalMenuCheckType checkable_type;
+ bool checked;
int max_states;
int state;
Ref<Image> img;
diff --git a/platform/macos/godot_menu_item.mm b/platform/macos/godot_menu_item.mm
index 30dac9be9b..479542113a 100644
--- a/platform/macos/godot_menu_item.mm
+++ b/platform/macos/godot_menu_item.mm
@@ -31,4 +31,18 @@
#include "godot_menu_item.h"
@implementation GodotMenuItem
+
+- (id)init {
+ self = [super init];
+
+ self->callback = Callable();
+ self->key_callback = Callable();
+ self->checkable_type = GlobalMenuCheckType::CHECKABLE_TYPE_NONE;
+ self->checked = false;
+ self->max_states = 0;
+ self->state = 0;
+
+ return self;
+}
+
@end
diff --git a/platform/macos/joypad_macos.mm b/platform/macos/joypad_macos.mm
index 8cd5cdd9f2..beb32d9129 100644
--- a/platform/macos/joypad_macos.mm
+++ b/platform/macos/joypad_macos.mm
@@ -228,7 +228,7 @@ void JoypadMacOS::joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp)
@property(assign, nonatomic) BOOL isObserving;
@property(assign, nonatomic) BOOL isProcessing;
@property(strong, nonatomic) NSMutableDictionary<NSNumber *, Joypad *> *connectedJoypads;
-@property(strong, nonatomic) NSMutableArray<Joypad *> *joypadsQueue;
+@property(strong, nonatomic) NSMutableArray<GCController *> *joypadsQueue;
@end
@@ -364,8 +364,7 @@ void JoypadMacOS::joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp)
if ([[self getAllKeysForController:controller] count] > 0) {
print_verbose("Controller is already registered.");
} else if (!self.isProcessing) {
- Joypad *joypad = [[Joypad alloc] init:controller];
- [self.joypadsQueue addObject:joypad];
+ [self.joypadsQueue addObject:controller];
} else {
[self addMacOSJoypad:controller];
}
diff --git a/platform/macos/native_menu_macos.mm b/platform/macos/native_menu_macos.mm
index 1ae1137ca0..802d58dc26 100644
--- a/platform/macos/native_menu_macos.mm
+++ b/platform/macos/native_menu_macos.mm
@@ -373,12 +373,7 @@ int NativeMenuMacOS::add_submenu_item(const RID &p_rid, const String &p_label, c
menu_item = [md->menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@"" atIndex:p_index];
GodotMenuItem *obj = [[GodotMenuItem alloc] init];
- obj->callback = Callable();
- obj->key_callback = Callable();
obj->meta = p_tag;
- obj->checkable_type = CHECKABLE_TYPE_NONE;
- obj->max_states = 0;
- obj->state = 0;
[menu_item setRepresentedObject:obj];
[md_sub->menu setTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()]];
@@ -417,9 +412,6 @@ int NativeMenuMacOS::add_item(const RID &p_rid, const String &p_label, const Cal
obj->callback = p_callback;
obj->key_callback = p_key_callback;
obj->meta = p_tag;
- obj->checkable_type = CHECKABLE_TYPE_NONE;
- obj->max_states = 0;
- obj->state = 0;
[menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)];
[menu_item setRepresentedObject:obj];
}
@@ -438,8 +430,6 @@ int NativeMenuMacOS::add_check_item(const RID &p_rid, const String &p_label, con
obj->key_callback = p_key_callback;
obj->meta = p_tag;
obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX;
- obj->max_states = 0;
- obj->state = 0;
[menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)];
[menu_item setRepresentedObject:obj];
}
@@ -457,9 +447,6 @@ int NativeMenuMacOS::add_icon_item(const RID &p_rid, const Ref<Texture2D> &p_ico
obj->callback = p_callback;
obj->key_callback = p_key_callback;
obj->meta = p_tag;
- obj->checkable_type = CHECKABLE_TYPE_NONE;
- obj->max_states = 0;
- obj->state = 0;
DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton();
if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) {
obj->img = p_icon->get_image();
@@ -489,8 +476,6 @@ int NativeMenuMacOS::add_icon_check_item(const RID &p_rid, const Ref<Texture2D>
obj->key_callback = p_key_callback;
obj->meta = p_tag;
obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX;
- obj->max_states = 0;
- obj->state = 0;
DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton();
if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) {
obj->img = p_icon->get_image();
@@ -520,8 +505,6 @@ int NativeMenuMacOS::add_radio_check_item(const RID &p_rid, const String &p_labe
obj->key_callback = p_key_callback;
obj->meta = p_tag;
obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON;
- obj->max_states = 0;
- obj->state = 0;
[menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)];
[menu_item setRepresentedObject:obj];
}
@@ -540,8 +523,6 @@ int NativeMenuMacOS::add_icon_radio_check_item(const RID &p_rid, const Ref<Textu
obj->key_callback = p_key_callback;
obj->meta = p_tag;
obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON;
- obj->max_states = 0;
- obj->state = 0;
DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton();
if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) {
obj->img = p_icon->get_image();
@@ -570,7 +551,6 @@ int NativeMenuMacOS::add_multistate_item(const RID &p_rid, const String &p_label
obj->callback = p_callback;
obj->key_callback = p_key_callback;
obj->meta = p_tag;
- obj->checkable_type = CHECKABLE_TYPE_NONE;
obj->max_states = p_max_states;
obj->state = p_default_state;
[menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)];
@@ -640,7 +620,10 @@ bool NativeMenuMacOS::is_item_checked(const RID &p_rid, int p_idx) const {
ERR_FAIL_COND_V(p_idx >= item_start + item_count, false);
const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx];
if (menu_item) {
- return ([menu_item state] == NSControlStateValueOn);
+ const GodotMenuItem *obj = [menu_item representedObject];
+ if (obj) {
+ return obj->checked;
+ }
}
return false;
}
@@ -958,10 +941,14 @@ void NativeMenuMacOS::set_item_checked(const RID &p_rid, int p_idx, bool p_check
ERR_FAIL_COND(p_idx >= item_start + item_count);
NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx];
if (menu_item) {
- if (p_checked) {
- [menu_item setState:NSControlStateValueOn];
- } else {
- [menu_item setState:NSControlStateValueOff];
+ GodotMenuItem *obj = [menu_item representedObject];
+ if (obj) {
+ obj->checked = p_checked;
+ if (p_checked) {
+ [menu_item setState:NSControlStateValueOn];
+ } else {
+ [menu_item setState:NSControlStateValueOff];
+ }
}
}
}
diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h
index 912a682a6b..303fc112bf 100644
--- a/platform/macos/os_macos.h
+++ b/platform/macos/os_macos.h
@@ -109,6 +109,7 @@ public:
virtual String get_executable_path() const override;
virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override;
virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override;
+ virtual bool is_process_running(const ProcessID &p_pid) const override;
virtual String get_unique_id() const override;
virtual String get_processor_name() const override;
diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm
index 9f0bea5951..3a82514766 100644
--- a/platform/macos/os_macos.mm
+++ b/platform/macos/os_macos.mm
@@ -666,6 +666,15 @@ Error OS_MacOS::create_instance(const List<String> &p_arguments, ProcessID *r_ch
}
}
+bool OS_MacOS::is_process_running(const ProcessID &p_pid) const {
+ NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:(pid_t)p_pid];
+ if (!app) {
+ return OS_Unix::is_process_running(p_pid);
+ }
+
+ return ![app isTerminated];
+}
+
String OS_MacOS::get_unique_id() const {
static String serial_number;
diff --git a/platform/macos/rendering_context_driver_vulkan_macos.mm b/platform/macos/rendering_context_driver_vulkan_macos.mm
index afefe5a6f7..b617cb8f26 100644
--- a/platform/macos/rendering_context_driver_vulkan_macos.mm
+++ b/platform/macos/rendering_context_driver_vulkan_macos.mm
@@ -50,7 +50,7 @@ RenderingContextDriver::SurfaceID RenderingContextDriverVulkanMacOS::surface_cre
create_info.pLayer = *wpd->layer_ptr;
VkSurfaceKHR vk_surface = VK_NULL_HANDLE;
- VkResult err = vkCreateMetalSurfaceEXT(instance_get(), &create_info, nullptr, &vk_surface);
+ VkResult err = vkCreateMetalSurfaceEXT(instance_get(), &create_info, get_allocation_callbacks(VK_OBJECT_TYPE_SURFACE_KHR), &vk_surface);
ERR_FAIL_COND_V(err != VK_SUCCESS, SurfaceID());
Surface *surface = memnew(Surface);
diff --git a/platform/web/audio_driver_web.cpp b/platform/web/audio_driver_web.cpp
index dd986e650c..0108f40726 100644
--- a/platform/web/audio_driver_web.cpp
+++ b/platform/web/audio_driver_web.cpp
@@ -33,6 +33,8 @@
#include "godot_audio.h"
#include "core/config/project_settings.h"
+#include "core/object/object.h"
+#include "scene/main/node.h"
#include "servers/audio/audio_stream.h"
#include <emscripten.h>
@@ -51,6 +53,21 @@ void AudioDriverWeb::_latency_update_callback(float p_latency) {
AudioDriverWeb::audio_context.output_latency = p_latency;
}
+void AudioDriverWeb::_sample_playback_finished_callback(const char *p_playback_object_id) {
+ const ObjectID playback_id = ObjectID(String::to_int(p_playback_object_id));
+
+ Object *playback_object = ObjectDB::get_instance(playback_id);
+ if (playback_object == nullptr) {
+ return;
+ }
+ Ref<AudioSamplePlayback> playback = Object::cast_to<AudioSamplePlayback>(playback_object);
+ if (playback.is_null()) {
+ return;
+ }
+
+ AudioServer::get_singleton()->stop_sample_playback(playback);
+}
+
void AudioDriverWeb::_audio_driver_process(int p_from, int p_samples) {
int32_t *stream_buffer = reinterpret_cast<int32_t *>(output_rb);
const int max_samples = memarr_len(output_rb);
@@ -132,6 +149,9 @@ Error AudioDriverWeb::init() {
if (!input_rb) {
return ERR_OUT_OF_MEMORY;
}
+
+ godot_audio_sample_set_finished_callback(&_sample_playback_finished_callback);
+
return OK;
}
@@ -274,6 +294,7 @@ void AudioDriverWeb::start_sample_playback(const Ref<AudioSamplePlayback> &p_pla
itos(p_playback->stream->get_instance_id()).utf8().get_data(),
AudioServer::get_singleton()->get_bus_index(p_playback->bus),
p_playback->offset,
+ p_playback->pitch_scale,
volume_ptrw);
}
@@ -292,6 +313,11 @@ bool AudioDriverWeb::is_sample_playback_active(const Ref<AudioSamplePlayback> &p
return godot_audio_sample_is_active(itos(p_playback->get_instance_id()).utf8().get_data()) != 0;
}
+double AudioDriverWeb::get_sample_playback_position(const Ref<AudioSamplePlayback> &p_playback) {
+ ERR_FAIL_COND_V_MSG(p_playback.is_null(), false, "Parameter p_playback is null.");
+ return godot_audio_get_sample_playback_position(itos(p_playback->get_instance_id()).utf8().get_data());
+}
+
void AudioDriverWeb::update_sample_playback_pitch_scale(const Ref<AudioSamplePlayback> &p_playback, float p_pitch_scale) {
ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null.");
godot_audio_sample_update_pitch_scale(
diff --git a/platform/web/audio_driver_web.h b/platform/web/audio_driver_web.h
index 298ad90fae..d352fa4692 100644
--- a/platform/web/audio_driver_web.h
+++ b/platform/web/audio_driver_web.h
@@ -58,6 +58,7 @@ private:
WASM_EXPORT static void _state_change_callback(int p_state);
WASM_EXPORT static void _latency_update_callback(float p_latency);
+ WASM_EXPORT static void _sample_playback_finished_callback(const char *p_playback_object_id);
static AudioDriverWeb *singleton;
@@ -95,6 +96,7 @@ public:
virtual void stop_sample_playback(const Ref<AudioSamplePlayback> &p_playback) override;
virtual void set_sample_playback_pause(const Ref<AudioSamplePlayback> &p_playback, bool p_paused) override;
virtual bool is_sample_playback_active(const Ref<AudioSamplePlayback> &p_playback) override;
+ virtual double get_sample_playback_position(const Ref<AudioSamplePlayback> &p_playback) override;
virtual void update_sample_playback_pitch_scale(const Ref<AudioSamplePlayback> &p_playback, float p_pitch_scale = 0.0f) override;
virtual void set_sample_playback_bus_volumes_linear(const Ref<AudioSamplePlayback> &p_playback, const HashMap<StringName, Vector<AudioFrame>> &p_bus_volumes) override;
diff --git a/platform/web/detect.py b/platform/web/detect.py
index cb4dac1125..bf75c2f9fc 100644
--- a/platform/web/detect.py
+++ b/platform/web/detect.py
@@ -78,6 +78,7 @@ def get_flags():
# -Os reduces file size by around 5 MiB over -O3. -Oz only saves about
# 100 KiB over -Os, which does not justify the negative impact on
# run-time performance.
+ # Note that this overrides the "auto" behavior for target/dev_build.
"optimize": "size",
}
@@ -226,6 +227,11 @@ def configure(env: "SConsEnvironment"):
env.Append(LINKFLAGS=["-sDEFAULT_PTHREAD_STACK_SIZE=%sKB" % env["default_pthread_stack_size"]])
env.Append(LINKFLAGS=["-sPTHREAD_POOL_SIZE=8"])
env.Append(LINKFLAGS=["-sWASM_MEM_MAX=2048MB"])
+ if not env["dlink_enabled"]:
+ # Workaround https://github.com/emscripten-core/emscripten/issues/21844#issuecomment-2116936414.
+ # Not needed (and potentially dangerous) when dlink_enabled=yes, since we set EXPORT_ALL=1 in that case.
+ env.Append(LINKFLAGS=["-sEXPORTED_FUNCTIONS=['__emscripten_thread_crashed','_main']"])
+
elif env["proxy_to_pthread"]:
print_warning('"threads=no" support requires "proxy_to_pthread=no", disabling proxy to pthread.')
env["proxy_to_pthread"] = False
diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp
index 40de4e523b..4e55cc137a 100644
--- a/platform/web/display_server_web.cpp
+++ b/platform/web/display_server_web.cpp
@@ -902,8 +902,10 @@ void DisplayServerWeb::process_joypads() {
for (int b = 0; b < s_btns_num; b++) {
// Buttons 6 and 7 in the standard mapping need to be
// axis to be handled as JoyAxis::TRIGGER by Godot.
- if (s_standard && (b == 6 || b == 7)) {
- input->joy_axis(idx, (JoyAxis)b, s_btns[b]);
+ if (s_standard && (b == 6)) {
+ input->joy_axis(idx, JoyAxis::TRIGGER_LEFT, s_btns[b]);
+ } else if (s_standard && (b == 7)) {
+ input->joy_axis(idx, JoyAxis::TRIGGER_RIGHT, s_btns[b]);
} else {
input->joy_button(idx, (JoyButton)b, s_btns[b]);
}
diff --git a/platform/web/emscripten_helpers.py b/platform/web/emscripten_helpers.py
index 2cee3e8110..8fcabb21c7 100644
--- a/platform/web/emscripten_helpers.py
+++ b/platform/web/emscripten_helpers.py
@@ -51,11 +51,13 @@ def create_template_zip(env, js, wasm, worker, side):
js,
wasm,
"#platform/web/js/libs/audio.worklet.js",
+ "#platform/web/js/libs/audio.position.worklet.js",
]
out_files = [
zip_dir.File(binary_name + ".js"),
zip_dir.File(binary_name + ".wasm"),
zip_dir.File(binary_name + ".audio.worklet.js"),
+ zip_dir.File(binary_name + ".audio.position.worklet.js"),
]
if env["threads"]:
in_files.append(worker)
@@ -74,6 +76,7 @@ def create_template_zip(env, js, wasm, worker, side):
"offline.html",
"godot.editor.js",
"godot.editor.audio.worklet.js",
+ "godot.editor.audio.position.worklet.js",
"logo.svg",
"favicon.png",
]
diff --git a/platform/web/export/export.cpp b/platform/web/export/export.cpp
index 168310c078..306ec624a0 100644
--- a/platform/web/export/export.cpp
+++ b/platform/web/export/export.cpp
@@ -40,7 +40,6 @@ void register_web_exporter_types() {
}
void register_web_exporter() {
-#ifndef ANDROID_ENABLED
EDITOR_DEF("export/web/http_host", "localhost");
EDITOR_DEF("export/web/http_port", 8060);
EDITOR_DEF("export/web/use_tls", false);
@@ -49,7 +48,6 @@ void register_web_exporter() {
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "export/web/http_port", PROPERTY_HINT_RANGE, "1,65535,1"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/web/tls_key", PROPERTY_HINT_GLOBAL_FILE, "*.key"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/web/tls_certificate", PROPERTY_HINT_GLOBAL_FILE, "*.crt,*.pem"));
-#endif
Ref<EditorExportPlatformWeb> platform;
platform.instantiate();
diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp
index d83e465e8e..d8c1b6033d 100644
--- a/platform/web/export/export_plugin.cpp
+++ b/platform/web/export/export_plugin.cpp
@@ -242,6 +242,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
}
cache_files.push_back(name + ".worker.js");
cache_files.push_back(name + ".audio.worklet.js");
+ cache_files.push_back(name + ".audio.position.worklet.js");
replaces["___GODOT_CACHE___"] = Variant(cache_files).to_json_string();
// Heavy files that are cached on demand.
@@ -835,6 +836,7 @@ Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_
DirAccess::remove_file_or_error(basepath + ".js");
DirAccess::remove_file_or_error(basepath + ".worker.js");
DirAccess::remove_file_or_error(basepath + ".audio.worklet.js");
+ DirAccess::remove_file_or_error(basepath + ".audio.position.worklet.js");
DirAccess::remove_file_or_error(basepath + ".service.worker.js");
DirAccess::remove_file_or_error(basepath + ".pck");
DirAccess::remove_file_or_error(basepath + ".png");
diff --git a/platform/web/godot_audio.h b/platform/web/godot_audio.h
index 8bebbcf7de..f5a2a85605 100644
--- a/platform/web/godot_audio.h
+++ b/platform/web/godot_audio.h
@@ -51,12 +51,14 @@ extern void godot_audio_input_stop();
extern int godot_audio_sample_stream_is_registered(const char *p_stream_object_id);
extern void godot_audio_sample_register_stream(const char *p_stream_object_id, float *p_frames_buf, int p_frames_total, const char *p_loop_mode, int p_loop_begin, int p_loop_end);
extern void godot_audio_sample_unregister_stream(const char *p_stream_object_id);
-extern void godot_audio_sample_start(const char *p_playback_object_id, const char *p_stream_object_id, int p_bus_index, float p_offset, float *p_volume_ptr);
+extern void godot_audio_sample_start(const char *p_playback_object_id, const char *p_stream_object_id, int p_bus_index, float p_offset, float p_pitch_scale, float *p_volume_ptr);
extern void godot_audio_sample_stop(const char *p_playback_object_id);
extern void godot_audio_sample_set_pause(const char *p_playback_object_id, bool p_pause);
extern int godot_audio_sample_is_active(const char *p_playback_object_id);
+extern double godot_audio_get_sample_playback_position(const char *p_playback_object_id);
extern void godot_audio_sample_update_pitch_scale(const char *p_playback_object_id, float p_pitch_scale);
extern void godot_audio_sample_set_volumes_linear(const char *p_playback_object_id, int *p_buses_buf, int p_buses_size, float *p_volumes_buf, int p_volumes_size);
+extern void godot_audio_sample_set_finished_callback(void (*p_callback)(const char *));
extern void godot_audio_sample_bus_set_count(int p_count);
extern void godot_audio_sample_bus_remove(int p_index);
diff --git a/platform/web/http_client_web.cpp b/platform/web/http_client_web.cpp
index ea9226a5a4..80257dc295 100644
--- a/platform/web/http_client_web.cpp
+++ b/platform/web/http_client_web.cpp
@@ -266,11 +266,11 @@ Error HTTPClientWeb::poll() {
return OK;
}
-HTTPClient *HTTPClientWeb::_create_func() {
- return memnew(HTTPClientWeb);
+HTTPClient *HTTPClientWeb::_create_func(bool p_notify_postinitialize) {
+ return static_cast<HTTPClient *>(ClassDB::creator<HTTPClientWeb>(p_notify_postinitialize));
}
-HTTPClient *(*HTTPClient::_create)() = HTTPClientWeb::_create_func;
+HTTPClient *(*HTTPClient::_create)(bool p_notify_postinitialize) = HTTPClientWeb::_create_func;
HTTPClientWeb::HTTPClientWeb() {
}
diff --git a/platform/web/http_client_web.h b/platform/web/http_client_web.h
index 4d3c457a7d..f696c5a5b0 100644
--- a/platform/web/http_client_web.h
+++ b/platform/web/http_client_web.h
@@ -81,7 +81,7 @@ private:
static void _parse_headers(int p_len, const char **p_headers, void *p_ref);
public:
- static HTTPClient *_create_func();
+ static HTTPClient *_create_func(bool p_notify_postinitialize);
Error request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_size) override;
diff --git a/platform/web/js/engine/config.js b/platform/web/js/engine/config.js
index 8c4e1b1b24..61b488cf81 100644
--- a/platform/web/js/engine/config.js
+++ b/platform/web/js/engine/config.js
@@ -299,6 +299,8 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
return `${loadPath}.worker.js`;
} else if (path.endsWith('.audio.worklet.js')) {
return `${loadPath}.audio.worklet.js`;
+ } else if (path.endsWith('.audio.position.worklet.js')) {
+ return `${loadPath}.audio.position.worklet.js`;
} else if (path.endsWith('.js')) {
return `${loadPath}.js`;
} else if (path in gdext) {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt b/platform/web/js/libs/audio.position.worklet.js
index 2df0195de7..bf3ac4ae2d 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt
+++ b/platform/web/js/libs/audio.position.worklet.js
@@ -1,5 +1,5 @@
/**************************************************************************/
-/* FileErrors.kt */
+/* godot.audio.position.worklet.js */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
@@ -28,26 +28,23 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
-package org.godotengine.godot.io.file
-
-/**
- * Set of errors that may occur when performing data access.
- */
-internal enum class FileErrors(val nativeValue: Int) {
- OK(0),
- FAILED(-1),
- FILE_NOT_FOUND(-2),
- FILE_CANT_OPEN(-3),
- INVALID_PARAMETER(-4);
+class GodotPositionReportingProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ this.position = 0;
+ }
- companion object {
- fun fromNativeError(error: Int): FileErrors? {
- for (fileError in entries) {
- if (fileError.nativeValue == error) {
- return fileError
- }
+ process(inputs, _outputs, _parameters) {
+ if (inputs.length > 0) {
+ const input = inputs[0];
+ if (input.length > 0) {
+ this.position += input[0].length;
+ this.port.postMessage({ 'type': 'position', 'data': this.position });
+ return true;
}
- return null
}
+ return true;
}
}
+
+registerProcessor('godot-position-reporting-processor', GodotPositionReportingProcessor);
diff --git a/platform/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js
index 531dbdaeab..40fb0c356c 100644
--- a/platform/web/js/libs/library_godot_audio.js
+++ b/platform/web/js/libs/library_godot_audio.js
@@ -77,7 +77,7 @@ class Sample {
* Creates a `Sample` based on the params. Will register it to the
* `GodotAudio.samples` registry.
* @param {SampleParams} params Base params
- * @param {SampleOptions} [options={}] Optional params
+ * @param {SampleOptions} [options={{}}] Optional params
* @returns {Sample}
*/
static create(params, options = {}) {
@@ -98,8 +98,7 @@ class Sample {
/**
* `Sample` constructor.
* @param {SampleParams} params Base params
- * @param {SampleOptions} [options={}] Optional params
- * @constructor
+ * @param {SampleOptions} [options={{}}] Optional params
*/
constructor(params, options = {}) {
/** @type {string} */
@@ -154,7 +153,7 @@ class Sample {
if (this._audioBuffer == null) {
throw new Error('couldn\'t duplicate a null audioBuffer');
}
- /** @type {Float32Array[]} */
+ /** @type {Array<Float32Array>} */
const channels = new Array(this._audioBuffer.numberOfChannels);
for (let i = 0; i < this._audioBuffer.numberOfChannels; i++) {
const channel = new Float32Array(this._audioBuffer.getChannelData(i));
@@ -189,7 +188,6 @@ class SampleNodeBus {
/**
* `SampleNodeBus` constructor.
* @param {Bus} bus The bus related to the new `SampleNodeBus`.
- * @constructor
*/
constructor(bus) {
const NUMBER_OF_WEB_CHANNELS = 6;
@@ -330,8 +328,10 @@ class SampleNodeBus {
* offset?: number
* playbackRate?: number
* startTime?: number
+ * pitchScale?: number
* loopMode?: LoopMode
* volume?: Float32Array
+ * start?: boolean
* }} SampleNodeOptions
*/
@@ -413,8 +413,7 @@ class SampleNode {
/**
* @param {SampleNodeParams} params Base params
- * @param {SampleNodeOptions} [options={}] Optional params
- * @constructor
+ * @param {SampleNodeOptions} [options={{}}] Optional params
*/
constructor(params, options = {}) {
/** @type {string} */
@@ -424,9 +423,15 @@ class SampleNode {
/** @type {number} */
this.offset = options.offset ?? 0;
/** @type {number} */
+ this._playbackPosition = options.offset;
+ /** @type {number} */
this.startTime = options.startTime ?? 0;
/** @type {boolean} */
this.isPaused = false;
+ /** @type {boolean} */
+ this.isStarted = false;
+ /** @type {boolean} */
+ this.isCanceled = false;
/** @type {number} */
this.pauseTime = 0;
/** @type {number} */
@@ -434,15 +439,17 @@ class SampleNode {
/** @type {LoopMode} */
this.loopMode = options.loopMode ?? this.getSample().loopMode ?? 'disabled';
/** @type {number} */
- this._pitchScale = 1;
+ this._pitchScale = options.pitchScale ?? 1;
/** @type {number} */
this._sourceStartTime = 0;
/** @type {Map<Bus, SampleNodeBus>} */
this._sampleNodeBuses = new Map();
/** @type {AudioBufferSourceNode | null} */
this._source = GodotAudio.ctx.createBufferSource();
- /** @type {AudioBufferSourceNode["onended"]} */
+
this._onended = null;
+ /** @type {AudioWorkletNode | null} */
+ this._positionWorklet = null;
this.setPlaybackRate(options.playbackRate ?? 44100);
this._source.buffer = this.getSample().getAudioBuffer();
@@ -452,6 +459,8 @@ class SampleNode {
const bus = GodotAudio.Bus.getBus(params.busIndex);
const sampleNodeBus = this.getSampleNodeBus(bus);
sampleNodeBus.setVolume(options.volume);
+
+ this.connectPositionWorklet(options.start);
}
/**
@@ -463,6 +472,14 @@ class SampleNode {
}
/**
+ * Gets the playback position.
+ * @returns {number}
+ */
+ getPlaybackPosition() {
+ return this._playbackPosition;
+ }
+
+ /**
* Sets the playback rate.
* @param {number} val Value to set.
* @returns {void}
@@ -511,8 +528,12 @@ class SampleNode {
* @returns {void}
*/
start() {
+ if (this.isStarted) {
+ return;
+ }
this._resetSourceStartTime();
this._source.start(this.startTime, this.offset);
+ this.isStarted = true;
}
/**
@@ -558,7 +579,7 @@ class SampleNode {
/**
* Sets the volumes of the `SampleNode` for each buses passed in parameters.
- * @param {Bus[]} buses
+ * @param {Array<Bus>} buses
* @param {Float32Array} volumes
*/
setVolumes(buses, volumes) {
@@ -588,17 +609,73 @@ class SampleNode {
}
/**
+ * Sets up and connects the source to the GodotPositionReportingProcessor
+ * If the worklet module is not loaded in, it will be added
+ */
+ connectPositionWorklet(start) {
+ try {
+ this._positionWorklet = this.createPositionWorklet();
+ this._source.connect(this._positionWorklet);
+ if (start) {
+ this.start();
+ }
+ } catch (error) {
+ if (error?.name !== 'InvalidStateError') {
+ throw error;
+ }
+ const path = GodotConfig.locate_file('godot.audio.position.worklet.js');
+ GodotAudio.ctx.audioWorklet
+ .addModule(path)
+ .then(() => {
+ if (!this.isCanceled) {
+ this._positionWorklet = this.createPositionWorklet();
+ this._source.connect(this._positionWorklet);
+ if (start) {
+ this.start();
+ }
+ }
+ }).catch((addModuleError) => {
+ GodotRuntime.error('Failed to create PositionWorklet.', addModuleError);
+ });
+ }
+ }
+
+ /**
+ * Creates the AudioWorkletProcessor used to track playback position.
+ * @returns {AudioWorkletNode}
+ */
+ createPositionWorklet() {
+ const worklet = new AudioWorkletNode(
+ GodotAudio.ctx,
+ 'godot-position-reporting-processor'
+ );
+ worklet.port.onmessage = (event) => {
+ switch (event.data['type']) {
+ case 'position':
+ this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset;
+ break;
+ default:
+ // Do nothing.
+ }
+ };
+ return worklet;
+ }
+
+ /**
* Clears the `SampleNode`.
* @returns {void}
*/
clear() {
+ this.isCanceled = true;
this.isPaused = false;
this.pauseTime = 0;
if (this._source != null) {
this._source.removeEventListener('ended', this._onended);
this._onended = null;
- this._source.stop();
+ if (this.isStarted) {
+ this._source.stop();
+ }
this._source.disconnect();
this._source = null;
}
@@ -608,6 +685,12 @@ class SampleNode {
}
this._sampleNodeBuses.clear();
+ if (this._positionWorklet) {
+ this._positionWorklet.disconnect();
+ this._positionWorklet.port.onmessage = null;
+ this._positionWorklet = null;
+ }
+
GodotAudio.SampleNode.delete(this.id);
}
@@ -633,7 +716,9 @@ class SampleNode {
* @returns {void}
*/
_restart() {
- this._source.disconnect();
+ if (this._source != null) {
+ this._source.disconnect();
+ }
this._source = GodotAudio.ctx.createBufferSource();
this._source.buffer = this.getSample().getAudioBuffer();
@@ -646,7 +731,9 @@ class SampleNode {
const pauseTime = this.isPaused
? this.pauseTime
: 0;
+ this.connectPositionWorklet();
this._source.start(this.startTime, this.offset + pauseTime);
+ this.isStarted = true;
}
/**
@@ -687,9 +774,15 @@ class SampleNode {
}
switch (self.getSample().loopMode) {
- case 'disabled':
+ case 'disabled': {
+ const id = this.id;
self.stop();
- break;
+ if (GodotAudio.sampleFinishedCallback != null) {
+ const idCharPtr = GodotRuntime.allocString(id);
+ GodotAudio.sampleFinishedCallback(idCharPtr);
+ GodotRuntime.free(idCharPtr);
+ }
+ } break;
case 'forward':
case 'backward':
self.restart();
@@ -812,7 +905,6 @@ class Bus {
/**
* `Bus` constructor.
- * @constructor
*/
constructor() {
/** @type {Set<SampleNode>} */
@@ -856,7 +948,10 @@ class Bus {
* @returns {void}
*/
setVolumeDb(val) {
- this._gainNode.gain.value = GodotAudio.db_to_linear(val);
+ const linear = GodotAudio.db_to_linear(val);
+ if (isFinite(linear)) {
+ this._gainNode.gain.value = linear;
+ }
}
/**
@@ -979,7 +1074,6 @@ class Bus {
GodotAudio.buses = GodotAudio.buses.filter((v) => v !== this);
}
- /** @type {Bus["prototype"]["_syncSampleNodes"]} */
_syncSampleNodes() {
const sampleNodes = Array.from(this._sampleNodes);
for (let i = 0; i < sampleNodes.length; i++) {
@@ -1080,7 +1174,7 @@ const _GodotAudio = {
// `Bus` class
/**
* Registry of `Bus`es.
- * @type {Bus[]}
+ * @type {Array<Bus>}
*/
buses: null,
/**
@@ -1090,6 +1184,12 @@ const _GodotAudio = {
busSolo: null,
Bus,
+ /**
+ * Callback to signal that a sample has finished.
+ * @type {(playbackObjectIdPtr: number) => void | null}
+ */
+ sampleFinishedCallback: null,
+
/** @type {AudioContext} */
ctx: null,
input: null,
@@ -1250,7 +1350,7 @@ const _GodotAudio = {
startOptions
) {
GodotAudio.SampleNode.stopSampleNode(playbackObjectId);
- const sampleNode = GodotAudio.SampleNode.create(
+ GodotAudio.SampleNode.create(
{
busIndex,
id: playbackObjectId,
@@ -1258,7 +1358,6 @@ const _GodotAudio = {
},
startOptions
);
- sampleNode.start();
},
/**
@@ -1297,7 +1396,7 @@ const _GodotAudio = {
/**
* Triggered when a sample node volumes need to be updated.
* @param {string} playbackObjectId Id of the sample playback
- * @param {number[]} busIndexes Indexes of the buses that need to be updated
+ * @param {Array<number>} busIndexes Indexes of the buses that need to be updated
* @param {Float32Array} volumes Array of the volumes
* @returns {void}
*/
@@ -1550,13 +1649,14 @@ const _GodotAudio = {
},
godot_audio_sample_start__proxy: 'sync',
- godot_audio_sample_start__sig: 'viiiii',
+ godot_audio_sample_start__sig: 'viiiifi',
/**
* Starts a sample.
* @param {number} playbackObjectIdStrPtr Playback object id pointer
* @param {number} streamObjectIdStrPtr Stream object id pointer
* @param {number} busIndex Bus index
* @param {number} offset Sample offset
+ * @param {number} pitchScale Pitch scale
* @param {number} volumePtr Volume pointer
* @returns {void}
*/
@@ -1565,6 +1665,7 @@ const _GodotAudio = {
streamObjectIdStrPtr,
busIndex,
offset,
+ pitchScale,
volumePtr
) {
/** @type {string} */
@@ -1573,11 +1674,13 @@ const _GodotAudio = {
const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr);
/** @type {Float32Array} */
const volume = GodotRuntime.heapSub(HEAPF32, volumePtr, 8);
- /** @type {SampleNodeConstructorOptions} */
+ /** @type {SampleNodeOptions} */
const startOptions = {
offset,
volume,
playbackRate: 1,
+ pitchScale,
+ start: true,
};
GodotAudio.start_sample(
playbackObjectId,
@@ -1623,6 +1726,22 @@ const _GodotAudio = {
return Number(GodotAudio.sampleNodes.has(playbackObjectId));
},
+ godot_audio_get_sample_playback_position__proxy: 'sync',
+ godot_audio_get_sample_playback_position__sig: 'di',
+ /**
+ * Returns the position of the playback position.
+ * @param {number} playbackObjectIdStrPtr Playback object id pointer
+ * @returns {number}
+ */
+ godot_audio_get_sample_playback_position: function (playbackObjectIdStrPtr) {
+ const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);
+ const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId);
+ if (sampleNode == null) {
+ return 0;
+ }
+ return sampleNode.getPlaybackPosition();
+ },
+
godot_audio_sample_update_pitch_scale__proxy: 'sync',
godot_audio_sample_update_pitch_scale__sig: 'vii',
/**
@@ -1764,6 +1883,17 @@ const _GodotAudio = {
godot_audio_sample_bus_set_mute: function (bus, enable) {
GodotAudio.set_sample_bus_mute(bus, Boolean(enable));
},
+
+ godot_audio_sample_set_finished_callback__proxy: 'sync',
+ godot_audio_sample_set_finished_callback__sig: 'vi',
+ /**
+ * Sets the finished callback
+ * @param {Number} callbackPtr Finished callback pointer
+ * @returns {void}
+ */
+ godot_audio_sample_set_finished_callback: function (callbackPtr) {
+ GodotAudio.sampleFinishedCallback = GodotRuntime.get_func(callbackPtr);
+ },
};
autoAddDeps(_GodotAudio, '$GodotAudio');
diff --git a/platform/web/js/libs/library_godot_input.js b/platform/web/js/libs/library_godot_input.js
index 7ea89d553f..6e3b97023d 100644
--- a/platform/web/js/libs/library_godot_input.js
+++ b/platform/web/js/libs/library_godot_input.js
@@ -112,6 +112,7 @@ const GodotIME = {
ime.style.top = '0px';
ime.style.width = '100%';
ime.style.height = '40px';
+ ime.style.pointerEvents = 'none';
ime.style.display = 'none';
ime.contentEditable = 'true';
diff --git a/platform/web/js/libs/library_godot_javascript_singleton.js b/platform/web/js/libs/library_godot_javascript_singleton.js
index b17fde1544..6bb69bca95 100644
--- a/platform/web/js/libs/library_godot_javascript_singleton.js
+++ b/platform/web/js/libs/library_godot_javascript_singleton.js
@@ -81,11 +81,16 @@ const GodotJSWrapper = {
case 0:
return null;
case 1:
- return !!GodotRuntime.getHeapValue(val, 'i64');
- case 2:
- return GodotRuntime.getHeapValue(val, 'i64');
+ return Boolean(GodotRuntime.getHeapValue(val, 'i64'));
+ case 2: {
+ // `heap_value` may be a bigint.
+ const heap_value = GodotRuntime.getHeapValue(val, 'i64');
+ return heap_value >= Number.MIN_SAFE_INTEGER && heap_value <= Number.MAX_SAFE_INTEGER
+ ? Number(heap_value)
+ : heap_value;
+ }
case 3:
- return GodotRuntime.getHeapValue(val, 'double');
+ return Number(GodotRuntime.getHeapValue(val, 'double'));
case 4:
return GodotRuntime.parseString(GodotRuntime.getHeapValue(val, '*'));
case 24: // OBJECT
@@ -110,6 +115,9 @@ const GodotJSWrapper = {
}
GodotRuntime.setHeapValue(p_exchange, p_val, 'double');
return 3; // FLOAT
+ } else if (type === 'bigint') {
+ GodotRuntime.setHeapValue(p_exchange, p_val, 'i64');
+ return 2; // INT
} else if (type === 'string') {
const c_str = GodotRuntime.allocString(p_val);
GodotRuntime.setHeapValue(p_exchange, c_str, '*');
diff --git a/platform/web/serve.py b/platform/web/serve.py
index f0b0ec9622..4e1521449b 100755
--- a/platform/web/serve.py
+++ b/platform/web/serve.py
@@ -6,7 +6,7 @@ import os
import socket
import subprocess
import sys
-from http.server import HTTPServer, SimpleHTTPRequestHandler, test # type: ignore
+from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
@@ -38,12 +38,24 @@ def shell_open(url):
def serve(root, port, run_browser):
os.chdir(root)
+ address = ("", port)
+ httpd = DualStackServer(address, CORSRequestHandler)
+
+ url = f"http://127.0.0.1:{port}"
if run_browser:
# Open the served page in the user's default browser.
- print("Opening the served URL in the default browser (use `--no-browser` or `-n` to disable this).")
- shell_open(f"http://127.0.0.1:{port}")
+ print(f"Opening the served URL in the default browser (use `--no-browser` or `-n` to disable this): {url}")
+ shell_open(url)
+ else:
+ print(f"Serving at: {url}")
- test(CORSRequestHandler, DualStackServer, port=port)
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ print("\nKeyboard interrupt received, stopping server.")
+ finally:
+ # Clean-up server
+ httpd.server_close()
if __name__ == "__main__":
diff --git a/platform/web/web_main.cpp b/platform/web/web_main.cpp
index 04513f6d57..d0c3bd7c0e 100644
--- a/platform/web/web_main.cpp
+++ b/platform/web/web_main.cpp
@@ -35,6 +35,8 @@
#include "core/config/engine.h"
#include "core/io/resource_loader.h"
#include "main/main.h"
+#include "scene/main/scene_tree.h"
+#include "scene/main/window.h" // SceneTree only forward declares it.
#include <emscripten/emscripten.h>
#include <stdlib.h>
@@ -130,7 +132,7 @@ extern EMSCRIPTEN_KEEPALIVE int godot_web_main(int argc, char *argv[]) {
if (Engine::get_singleton()->is_project_manager_hint() && FileAccess::exists("/tmp/preload.zip")) {
PackedStringArray ps;
ps.push_back("/tmp/preload.zip");
- os->get_main_loop()->emit_signal(SNAME("files_dropped"), ps, -1);
+ SceneTree::get_singleton()->get_root()->emit_signal(SNAME("files_dropped"), ps);
}
#endif
emscripten_set_main_loop(main_loop_callback, -1, false);
diff --git a/platform/windows/SCsub b/platform/windows/SCsub
index f2fb8616ae..f8ed8b73f5 100644
--- a/platform/windows/SCsub
+++ b/platform/windows/SCsub
@@ -108,18 +108,6 @@ if env["d3d12"]:
# Used in cases where we can have multiple archs side-by-side.
arch_bin_dir = "#bin/" + env["arch"]
- # DXC
- if env["dxc_path"] != "" and os.path.exists(env["dxc_path"]):
- dxc_dll = "dxil.dll"
- # Whether this one is loaded from arch-specific directory or not can be determined at runtime.
- # Let's copy to both and let the user decide the distribution model.
- for v in ["#bin", arch_bin_dir]:
- env.Command(
- v + "/" + dxc_dll,
- env["dxc_path"] + "/bin/" + dxc_arch_subdir + "/" + dxc_dll,
- Copy("$TARGET", "$SOURCE"),
- )
-
# Agility SDK
if env["agility_sdk_path"] != "" and os.path.exists(env["agility_sdk_path"]):
agility_dlls = ["D3D12Core.dll", "d3d12SDKLayers.dll"]
diff --git a/platform/windows/detect.py b/platform/windows/detect.py
index fee306a25c..11dd4548f1 100644
--- a/platform/windows/detect.py
+++ b/platform/windows/detect.py
@@ -214,11 +214,6 @@ def get_opts():
os.path.join(d3d12_deps_folder, "mesa"),
),
(
- "dxc_path",
- "Path to the DirectX Shader Compiler distribution (required for D3D12)",
- os.path.join(d3d12_deps_folder, "dxc"),
- ),
- (
"agility_sdk_path",
"Path to the Agility SDK distribution (optional for D3D12)",
os.path.join(d3d12_deps_folder, "agility_sdk"),
@@ -252,7 +247,7 @@ def get_flags():
return {
"arch": arch,
- "supported": ["mono"],
+ "supported": ["d3d12", "mono", "xaudio2"],
}
@@ -306,7 +301,6 @@ def setup_msvc_manual(env: "SConsEnvironment"):
print("Using VCVARS-determined MSVC, arch %s" % (env_arch))
-# FIXME: Likely overwrites command-line options for the msvc compiler. See #91883.
def setup_msvc_auto(env: "SConsEnvironment"):
"""Set up MSVC using SCons's auto-detection logic"""
@@ -339,6 +333,12 @@ def setup_msvc_auto(env: "SConsEnvironment"):
env.Tool("msvc")
env.Tool("mssdk") # we want the MS SDK
+ # Re-add potentially overwritten flags.
+ env.AppendUnique(CCFLAGS=env.get("ccflags", "").split())
+ env.AppendUnique(CXXFLAGS=env.get("cxxflags", "").split())
+ env.AppendUnique(CFLAGS=env.get("cflags", "").split())
+ env.AppendUnique(RCFLAGS=env.get("rcflags", "").split())
+
# Note: actual compiler version can be found in env['MSVC_VERSION'], e.g. "14.1" for VS2015
print("Using SCons-detected MSVC version %s, arch %s" % (env["MSVC_VERSION"], env["arch"]))
@@ -467,6 +467,8 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config):
if env["arch"] == "x86_32":
env["x86_libtheora_opt_vc"] = True
+ env.Append(CCFLAGS=["/fp:strict"])
+
env.AppendUnique(CCFLAGS=["/Gd", "/GR", "/nologo"])
env.AppendUnique(CCFLAGS=["/utf-8"]) # Force to use Unicode encoding.
env.AppendUnique(CXXFLAGS=["/TP"]) # assume all sources are C++
@@ -644,7 +646,8 @@ def configure_mingw(env: "SConsEnvironment"):
# TODO: Re-evaluate the need for this / streamline with common config.
if env["target"] == "template_release":
- env.Append(CCFLAGS=["-msse2"])
+ if env["arch"] != "arm64":
+ env.Append(CCFLAGS=["-msse2"])
elif env.dev_build:
# Allow big objects. It's supposed not to have drawbacks but seems to break
# GCC LTO, so enabling for debug builds only (which are not built with LTO
@@ -674,6 +677,8 @@ def configure_mingw(env: "SConsEnvironment"):
if env["arch"] in ["x86_32", "x86_64"]:
env["x86_libtheora_opt_gcc"] = True
+ env.Append(CCFLAGS=["-ffp-contract=off"])
+
mingw_bin_prefix = get_mingw_bin_prefix(env["mingw_prefix"], env["arch"])
if env["use_llvm"]:
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index 8d26a705a9..270112e624 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -38,6 +38,7 @@
#include "core/version.h"
#include "drivers/png/png_driver_common.h"
#include "main/main.h"
+#include "scene/resources/texture.h"
#if defined(VULKAN_ENABLED)
#include "rendering_context_driver_vulkan_windows.h"
@@ -132,9 +133,17 @@ String DisplayServerWindows::get_name() const {
}
void DisplayServerWindows::_set_mouse_mode_impl(MouseMode p_mode) {
+ if (p_mode == MOUSE_MODE_HIDDEN || p_mode == MOUSE_MODE_CAPTURED || p_mode == MOUSE_MODE_CONFINED_HIDDEN) {
+ // Hide cursor before moving.
+ if (hCursor == nullptr) {
+ hCursor = SetCursor(nullptr);
+ } else {
+ SetCursor(nullptr);
+ }
+ }
+
if (windows.has(MAIN_WINDOW_ID) && (p_mode == MOUSE_MODE_CAPTURED || p_mode == MOUSE_MODE_CONFINED || p_mode == MOUSE_MODE_CONFINED_HIDDEN)) {
// Mouse is grabbed (captured or confined).
-
WindowID window_id = _get_focused_window_or_popup();
if (!windows.has(window_id)) {
window_id = MAIN_WINDOW_ID;
@@ -164,13 +173,8 @@ void DisplayServerWindows::_set_mouse_mode_impl(MouseMode p_mode) {
_register_raw_input_devices(INVALID_WINDOW_ID);
}
- if (p_mode == MOUSE_MODE_HIDDEN || p_mode == MOUSE_MODE_CAPTURED || p_mode == MOUSE_MODE_CONFINED_HIDDEN) {
- if (hCursor == nullptr) {
- hCursor = SetCursor(nullptr);
- } else {
- SetCursor(nullptr);
- }
- } else {
+ if (p_mode == MOUSE_MODE_VISIBLE || p_mode == MOUSE_MODE_CONFINED) {
+ // Show cursor.
CursorShape c = cursor_shape;
cursor_shape = CURSOR_MAX;
cursor_set_shape(c);
@@ -249,6 +253,14 @@ void DisplayServerWindows::tts_stop() {
tts->stop();
}
+Error DisplayServerWindows::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
+ return _file_dialog_with_options_show(p_title, p_current_directory, String(), p_filename, p_show_hidden, p_mode, p_filters, TypedArray<Dictionary>(), p_callback, false);
+}
+
+Error DisplayServerWindows::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) {
+ return _file_dialog_with_options_show(p_title, p_current_directory, p_root, p_filename, p_show_hidden, p_mode, p_filters, p_options, p_callback, true);
+}
+
// Silence warning due to a COM API weirdness.
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push
@@ -376,22 +388,85 @@ public:
#pragma GCC diagnostic pop
#endif
-Error DisplayServerWindows::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
- return _file_dialog_with_options_show(p_title, p_current_directory, String(), p_filename, p_show_hidden, p_mode, p_filters, TypedArray<Dictionary>(), p_callback, false);
+LRESULT CALLBACK WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+ DisplayServerWindows *ds_win = static_cast<DisplayServerWindows *>(DisplayServer::get_singleton());
+ if (ds_win) {
+ return ds_win->WndProcFileDialog(hWnd, uMsg, wParam, lParam);
+ } else {
+ return DefWindowProcW(hWnd, uMsg, wParam, lParam);
+ }
}
-Error DisplayServerWindows::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) {
- return _file_dialog_with_options_show(p_title, p_current_directory, p_root, p_filename, p_show_hidden, p_mode, p_filters, p_options, p_callback, true);
+LRESULT DisplayServerWindows::WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+ MutexLock lock(file_dialog_mutex);
+ if (file_dialog_wnd.has(hWnd)) {
+ if (file_dialog_wnd[hWnd]->close_requested.is_set()) {
+ IPropertyStore *prop_store;
+ HRESULT hr = SHGetPropertyStoreForWindow(hWnd, IID_IPropertyStore, (void **)&prop_store);
+ if (hr == S_OK) {
+ PROPVARIANT val;
+ PropVariantInit(&val);
+ prop_store->SetValue(PKEY_AppUserModel_ID, val);
+ prop_store->Release();
+ }
+ DestroyWindow(hWnd);
+ file_dialog_wnd.erase(hWnd);
+ }
+ }
+ return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}
-Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb) {
- _THREAD_SAFE_METHOD_
+void DisplayServerWindows::_thread_fd_monitor(void *p_ud) {
+ DisplayServerWindows *ds = static_cast<DisplayServerWindows *>(get_singleton());
+ FileDialogData *fd = (FileDialogData *)p_ud;
- ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED);
+ if (fd->mode < 0 && fd->mode >= DisplayServer::FILE_DIALOG_MODE_SAVE_MAX) {
+ fd->finished.set();
+ return;
+ }
+ CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ int64_t x = fd->wrect.position.x;
+ int64_t y = fd->wrect.position.y;
+ int64_t w = fd->wrect.size.x;
+ int64_t h = fd->wrect.size.y;
+
+ WNDCLASSW wc = {};
+ wc.lpfnWndProc = (WNDPROC)::WndProcFileDialog;
+ wc.hInstance = GetModuleHandle(nullptr);
+ wc.lpszClassName = L"Engine File Dialog";
+ RegisterClassW(&wc);
+
+ HWND hwnd_dialog = CreateWindowExW(WS_EX_APPWINDOW, L"Engine File Dialog", L"", WS_OVERLAPPEDWINDOW, x, y, w, h, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
+ if (hwnd_dialog) {
+ {
+ MutexLock lock(ds->file_dialog_mutex);
+ ds->file_dialog_wnd[hwnd_dialog] = fd;
+ }
+
+ HICON mainwindow_icon = (HICON)SendMessage(fd->hwnd_owner, WM_GETICON, ICON_SMALL, 0);
+ if (mainwindow_icon) {
+ SendMessage(hwnd_dialog, WM_SETICON, ICON_SMALL, (LPARAM)mainwindow_icon);
+ }
+ mainwindow_icon = (HICON)SendMessage(fd->hwnd_owner, WM_GETICON, ICON_BIG, 0);
+ if (mainwindow_icon) {
+ SendMessage(hwnd_dialog, WM_SETICON, ICON_BIG, (LPARAM)mainwindow_icon);
+ }
+ IPropertyStore *prop_store;
+ HRESULT hr = SHGetPropertyStoreForWindow(hwnd_dialog, IID_IPropertyStore, (void **)&prop_store);
+ if (hr == S_OK) {
+ PROPVARIANT val;
+ InitPropVariantFromString((PCWSTR)fd->appid.utf16().get_data(), &val);
+ prop_store->SetValue(PKEY_AppUserModel_ID, val);
+ prop_store->Release();
+ }
+ }
+
+ SetCurrentProcessExplicitAppUserModelID((PCWSTR)fd->appid.utf16().get_data());
Vector<Char16String> filter_names;
Vector<Char16String> filter_exts;
- for (const String &E : p_filters) {
+ for (const String &E : fd->filters) {
Vector<String> tokens = E.split(";");
if (tokens.size() >= 1) {
String flt = tokens[0].strip_edges();
@@ -424,11 +499,9 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
filters.push_back({ (LPCWSTR)filter_names[i].ptr(), (LPCWSTR)filter_exts[i].ptr() });
}
- WindowID prev_focus = last_focused_window;
-
HRESULT hr = S_OK;
IFileDialog *pfd = nullptr;
- if (p_mode == FILE_DIALOG_MODE_SAVE_FILE) {
+ if (fd->mode == DisplayServer::FILE_DIALOG_MODE_SAVE_FILE) {
hr = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileSaveDialog, (void **)&pfd);
} else {
hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileOpenDialog, (void **)&pfd);
@@ -444,40 +517,32 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
IFileDialogCustomize *pfdc = nullptr;
hr = pfd->QueryInterface(IID_PPV_ARGS(&pfdc));
- for (int i = 0; i < p_options.size(); i++) {
- const Dictionary &item = p_options[i];
+ for (int i = 0; i < fd->options.size(); i++) {
+ const Dictionary &item = fd->options[i];
if (!item.has("name") || !item.has("values") || !item.has("default")) {
continue;
}
- const String &name = item["name"];
- const Vector<String> &options = item["values"];
- int default_idx = item["default"];
-
- event_handler->add_option(pfdc, name, options, default_idx);
+ event_handler->add_option(pfdc, item["name"], item["values"], item["default_idx"]);
}
- event_handler->set_root(p_root);
+ event_handler->set_root(fd->root);
pfdc->Release();
DWORD flags;
pfd->GetOptions(&flags);
- if (p_mode == FILE_DIALOG_MODE_OPEN_FILES) {
+ if (fd->mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES) {
flags |= FOS_ALLOWMULTISELECT;
}
- if (p_mode == FILE_DIALOG_MODE_OPEN_DIR) {
+ if (fd->mode == DisplayServer::FILE_DIALOG_MODE_OPEN_DIR) {
flags |= FOS_PICKFOLDERS;
}
- if (p_show_hidden) {
+ if (fd->show_hidden) {
flags |= FOS_FORCESHOWHIDDEN;
}
pfd->SetOptions(flags | FOS_FORCEFILESYSTEM);
- pfd->SetTitle((LPCWSTR)p_title.utf16().ptr());
+ pfd->SetTitle((LPCWSTR)fd->title.utf16().ptr());
- String dir = ProjectSettings::get_singleton()->globalize_path(p_current_directory);
- if (dir == ".") {
- dir = OS::get_singleton()->get_executable_path().get_base_dir();
- }
- dir = dir.replace("/", "\\");
+ String dir = fd->current_directory.replace("/", "\\");
IShellItem *shellitem = nullptr;
hr = SHCreateItemFromParsingName((LPCWSTR)dir.utf16().ptr(), nullptr, IID_IShellItem, (void **)&shellitem);
@@ -486,16 +551,11 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
pfd->SetFolder(shellitem);
}
- pfd->SetFileName((LPCWSTR)p_filename.utf16().ptr());
+ pfd->SetFileName((LPCWSTR)fd->filename.utf16().ptr());
pfd->SetFileTypes(filters.size(), filters.ptr());
pfd->SetFileTypeIndex(0);
- WindowID window_id = _get_focused_window_or_popup();
- if (!windows.has(window_id)) {
- window_id = MAIN_WINDOW_ID;
- }
-
- hr = pfd->Show(windows[window_id].hWnd);
+ hr = pfd->Show(hwnd_dialog);
pfd->Unadvise(cookie);
Dictionary options = event_handler->get_selected();
@@ -512,7 +572,7 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
if (SUCCEEDED(hr)) {
Vector<String> file_names;
- if (p_mode == FILE_DIALOG_MODE_OPEN_FILES) {
+ if (fd->mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES) {
IShellItemArray *results;
hr = static_cast<IFileOpenDialog *>(pfd)->GetResults(&results);
if (SUCCEEDED(hr)) {
@@ -545,73 +605,148 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
result->Release();
}
}
- if (p_callback.is_valid()) {
- if (p_options_in_cb) {
+ if (fd->callback.is_valid()) {
+ if (fd->options_in_cb) {
Variant v_result = true;
Variant v_files = file_names;
Variant v_index = index;
Variant v_opt = options;
- Variant ret;
- Callable::CallError ce;
- const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
+ const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt };
- p_callback.callp(args, 4, ret, ce);
- if (ce.error != Callable::CallError::CALL_OK) {
- ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 4, ce)));
- }
+ fd->callback.call_deferredp(cb_args, 4);
} else {
Variant v_result = true;
Variant v_files = file_names;
Variant v_index = index;
- Variant ret;
- Callable::CallError ce;
- const Variant *args[3] = { &v_result, &v_files, &v_index };
+ const Variant *cb_args[3] = { &v_result, &v_files, &v_index };
- p_callback.callp(args, 3, ret, ce);
- if (ce.error != Callable::CallError::CALL_OK) {
- ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce)));
- }
+ fd->callback.call_deferredp(cb_args, 3);
}
}
} else {
- if (p_callback.is_valid()) {
- if (p_options_in_cb) {
+ if (fd->callback.is_valid()) {
+ if (fd->options_in_cb) {
Variant v_result = false;
Variant v_files = Vector<String>();
- Variant v_index = index;
- Variant v_opt = options;
- Variant ret;
- Callable::CallError ce;
- const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
+ Variant v_index = 0;
+ Variant v_opt = Dictionary();
+ const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt };
- p_callback.callp(args, 4, ret, ce);
- if (ce.error != Callable::CallError::CALL_OK) {
- ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 4, ce)));
- }
+ fd->callback.call_deferredp(cb_args, 4);
} else {
Variant v_result = false;
Variant v_files = Vector<String>();
- Variant v_index = index;
- Variant ret;
- Callable::CallError ce;
- const Variant *args[3] = { &v_result, &v_files, &v_index };
+ Variant v_index = 0;
+ const Variant *cb_args[3] = { &v_result, &v_files, &v_index };
- p_callback.callp(args, 3, ret, ce);
- if (ce.error != Callable::CallError::CALL_OK) {
- ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce)));
- }
+ fd->callback.call_deferredp(cb_args, 3);
}
}
}
pfd->Release();
- if (prev_focus != INVALID_WINDOW_ID) {
- callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(prev_focus);
+ } else {
+ if (fd->callback.is_valid()) {
+ if (fd->options_in_cb) {
+ Variant v_result = false;
+ Variant v_files = Vector<String>();
+ Variant v_index = 0;
+ Variant v_opt = Dictionary();
+ const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt };
+
+ fd->callback.call_deferredp(cb_args, 4);
+ } else {
+ Variant v_result = false;
+ Variant v_files = Vector<String>();
+ Variant v_index = 0;
+ const Variant *cb_args[3] = { &v_result, &v_files, &v_index };
+
+ fd->callback.call_deferredp(cb_args, 3);
+ }
+ }
+ }
+ {
+ MutexLock lock(ds->file_dialog_mutex);
+ if (hwnd_dialog && ds->file_dialog_wnd.has(hwnd_dialog)) {
+ IPropertyStore *prop_store;
+ hr = SHGetPropertyStoreForWindow(hwnd_dialog, IID_IPropertyStore, (void **)&prop_store);
+ if (hr == S_OK) {
+ PROPVARIANT val;
+ PropVariantInit(&val);
+ prop_store->SetValue(PKEY_AppUserModel_ID, val);
+ prop_store->Release();
+ }
+ DestroyWindow(hwnd_dialog);
+ ds->file_dialog_wnd.erase(hwnd_dialog);
}
+ }
+ UnregisterClassW(L"Engine File Dialog", GetModuleHandle(nullptr));
+ CoUninitialize();
+
+ fd->finished.set();
+
+ if (fd->window_id != INVALID_WINDOW_ID) {
+ callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd->window_id);
+ }
+}
+
+Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb) {
+ _THREAD_SAFE_METHOD_
+
+ ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED);
+
+ WindowID window_id = _get_focused_window_or_popup();
+ if (!windows.has(window_id)) {
+ window_id = MAIN_WINDOW_ID;
+ }
+ String appname;
+ if (Engine::get_singleton()->is_editor_hint()) {
+ appname = "Godot.GodotEditor." + String(VERSION_BRANCH);
+ } else {
+ String name = GLOBAL_GET("application/config/name");
+ String version = GLOBAL_GET("application/config/version");
+ if (version.is_empty()) {
+ version = "0";
+ }
+ String clean_app_name = name.to_pascal_case();
+ for (int i = 0; i < clean_app_name.length(); i++) {
+ if (!is_ascii_alphanumeric_char(clean_app_name[i]) && clean_app_name[i] != '_' && clean_app_name[i] != '.') {
+ clean_app_name[i] = '_';
+ }
+ }
+ clean_app_name = clean_app_name.substr(0, 120 - version.length()).trim_suffix(".");
+ appname = "Godot." + clean_app_name + "." + version;
+ }
- return OK;
+ FileDialogData *fd = memnew(FileDialogData);
+ if (window_id != INVALID_WINDOW_ID) {
+ fd->hwnd_owner = windows[window_id].hWnd;
+ RECT crect;
+ GetWindowRect(fd->hwnd_owner, &crect);
+ fd->wrect = Rect2i(crect.left, crect.top, crect.right - crect.left, crect.bottom - crect.top);
} else {
- return ERR_CANT_OPEN;
+ fd->hwnd_owner = 0;
+ fd->wrect = Rect2i(CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT);
}
+ fd->appid = appname;
+ fd->title = p_title;
+ fd->current_directory = p_current_directory;
+ fd->root = p_root;
+ fd->filename = p_filename;
+ fd->show_hidden = p_show_hidden;
+ fd->mode = p_mode;
+ fd->window_id = window_id;
+ fd->filters = p_filters;
+ fd->options = p_options;
+ fd->callback = p_callback;
+ fd->options_in_cb = p_options_in_cb;
+ fd->finished.clear();
+ fd->close_requested.clear();
+
+ fd->listener_thread.start(DisplayServerWindows::_thread_fd_monitor, fd);
+
+ file_dialogs.push_back(fd);
+
+ return OK;
}
void DisplayServerWindows::mouse_set_mode(MouseMode p_mode) {
@@ -1305,10 +1440,10 @@ DisplayServer::WindowID DisplayServerWindows::get_window_at_screen_position(cons
return INVALID_WINDOW_ID;
}
-DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect) {
+DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect, bool p_exclusive, WindowID p_transient_parent) {
_THREAD_SAFE_METHOD_
- WindowID window_id = _create_window(p_mode, p_vsync_mode, p_flags, p_rect);
+ WindowID window_id = _create_window(p_mode, p_vsync_mode, p_flags, p_rect, p_exclusive, p_transient_parent);
ERR_FAIL_COND_V_MSG(window_id == INVALID_WINDOW_ID, INVALID_WINDOW_ID, "Failed to create sub window.");
WindowData &wd = windows[window_id];
@@ -1332,13 +1467,15 @@ DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mod
wd.is_popup = true;
}
if (p_flags & WINDOW_FLAG_TRANSPARENT_BIT) {
- DWM_BLURBEHIND bb;
- ZeroMemory(&bb, sizeof(bb));
- HRGN hRgn = CreateRectRgn(0, 0, -1, -1);
- bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
- bb.hRgnBlur = hRgn;
- bb.fEnable = TRUE;
- DwmEnableBlurBehindWindow(wd.hWnd, &bb);
+ if (OS::get_singleton()->is_layered_allowed()) {
+ DWM_BLURBEHIND bb;
+ ZeroMemory(&bb, sizeof(bb));
+ HRGN hRgn = CreateRectRgn(0, 0, -1, -1);
+ bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
+ bb.hRgnBlur = hRgn;
+ bb.fEnable = TRUE;
+ DwmEnableBlurBehindWindow(wd.hWnd, &bb);
+ }
wd.layered_window = true;
}
@@ -2008,7 +2145,7 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window)
}
if (p_mode == WINDOW_MODE_WINDOWED) {
- ShowWindow(wd.hWnd, SW_RESTORE);
+ ShowWindow(wd.hWnd, SW_NORMAL);
wd.maximized = false;
wd.minimized = false;
}
@@ -2118,28 +2255,29 @@ void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, W
} break;
case WINDOW_FLAG_TRANSPARENT: {
if (p_enabled) {
- //enable per-pixel alpha
-
- DWM_BLURBEHIND bb;
- ZeroMemory(&bb, sizeof(bb));
- HRGN hRgn = CreateRectRgn(0, 0, -1, -1);
- bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
- bb.hRgnBlur = hRgn;
- bb.fEnable = TRUE;
- DwmEnableBlurBehindWindow(wd.hWnd, &bb);
-
+ // Enable per-pixel alpha.
+ if (OS::get_singleton()->is_layered_allowed()) {
+ DWM_BLURBEHIND bb;
+ ZeroMemory(&bb, sizeof(bb));
+ HRGN hRgn = CreateRectRgn(0, 0, -1, -1);
+ bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
+ bb.hRgnBlur = hRgn;
+ bb.fEnable = TRUE;
+ DwmEnableBlurBehindWindow(wd.hWnd, &bb);
+ }
wd.layered_window = true;
} else {
- //disable per-pixel alpha
+ // Disable per-pixel alpha.
wd.layered_window = false;
-
- DWM_BLURBEHIND bb;
- ZeroMemory(&bb, sizeof(bb));
- HRGN hRgn = CreateRectRgn(0, 0, -1, -1);
- bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
- bb.hRgnBlur = hRgn;
- bb.fEnable = FALSE;
- DwmEnableBlurBehindWindow(wd.hWnd, &bb);
+ if (OS::get_singleton()->is_layered_allowed()) {
+ DWM_BLURBEHIND bb;
+ ZeroMemory(&bb, sizeof(bb));
+ HRGN hRgn = CreateRectRgn(0, 0, -1, -1);
+ bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
+ bb.hRgnBlur = hRgn;
+ bb.fEnable = FALSE;
+ DwmEnableBlurBehindWindow(wd.hWnd, &bb);
+ }
}
} break;
case WINDOW_FLAG_NO_FOCUS: {
@@ -2910,24 +3048,67 @@ Key DisplayServerWindows::keyboard_get_label_from_physical(Key p_keycode) const
return p_keycode;
}
-String _get_full_layout_name_from_registry(HKL p_layout) {
- String id = "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\" + String::num_int64((int64_t)p_layout, 16, false).lpad(8, "0");
+String DisplayServerWindows::_get_keyboard_layout_display_name(const String &p_klid) const {
String ret;
+ HKEY key;
+ if (RegOpenKeyW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", &key) != ERROR_SUCCESS) {
+ return String();
+ }
- HKEY hkey;
- WCHAR layout_text[1024];
- memset(layout_text, 0, 1024 * sizeof(WCHAR));
-
- if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, (LPCWSTR)(id.utf16().get_data()), 0, KEY_QUERY_VALUE, &hkey) != ERROR_SUCCESS) {
- return ret;
+ WCHAR buffer[MAX_PATH] = {};
+ DWORD buffer_size = MAX_PATH;
+ if (RegGetValueW(key, (LPCWSTR)p_klid.utf16().get_data(), L"Layout Display Name", RRF_RT_REG_SZ, nullptr, buffer, &buffer_size) == ERROR_SUCCESS) {
+ if (load_indirect_string) {
+ if (load_indirect_string(buffer, buffer, buffer_size, nullptr) == S_OK) {
+ ret = String::utf16((const char16_t *)buffer, buffer_size);
+ }
+ }
+ } else {
+ if (RegGetValueW(key, (LPCWSTR)p_klid.utf16().get_data(), L"Layout Text", RRF_RT_REG_SZ, nullptr, buffer, &buffer_size) == ERROR_SUCCESS) {
+ ret = String::utf16((const char16_t *)buffer, buffer_size);
+ }
}
- DWORD buffer = 1024;
- DWORD vtype = REG_SZ;
- if (RegQueryValueExW(hkey, L"Layout Text", nullptr, &vtype, (LPBYTE)layout_text, &buffer) == ERROR_SUCCESS) {
- ret = String::utf16((const char16_t *)layout_text);
+ RegCloseKey(key);
+ return ret;
+}
+
+String DisplayServerWindows::_get_klid(HKL p_hkl) const {
+ String ret;
+
+ WORD device = HIWORD(p_hkl);
+ if ((device & 0xf000) == 0xf000) {
+ WORD layout_id = device & 0x0fff;
+
+ HKEY key;
+ if (RegOpenKeyW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", &key) != ERROR_SUCCESS) {
+ return String();
+ }
+
+ DWORD index = 0;
+ wchar_t klid_buffer[KL_NAMELENGTH];
+ DWORD klid_buffer_size = KL_NAMELENGTH;
+ while (RegEnumKeyExW(key, index, klid_buffer, &klid_buffer_size, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) {
+ wchar_t layout_id_buf[MAX_PATH] = {};
+ DWORD layout_id_size = MAX_PATH;
+ if (RegGetValueW(key, klid_buffer, L"Layout Id", RRF_RT_REG_SZ, nullptr, layout_id_buf, &layout_id_size) == ERROR_SUCCESS) {
+ if (layout_id == String::utf16((char16_t *)layout_id_buf, layout_id_size).hex_to_int()) {
+ ret = String::utf16((const char16_t *)klid_buffer, klid_buffer_size).lpad(8, "0");
+ break;
+ }
+ }
+ klid_buffer_size = KL_NAMELENGTH;
+ ++index;
+ }
+
+ RegCloseKey(key);
+ } else {
+ if (device == 0) {
+ device = LOWORD(p_hkl);
+ }
+ ret = (String::num_uint64((uint64_t)device, 16, false)).lpad(8, "0");
}
- RegCloseKey(hkey);
+
return ret;
}
@@ -2939,7 +3120,7 @@ String DisplayServerWindows::keyboard_get_layout_name(int p_index) const {
HKL *layouts = (HKL *)memalloc(layout_count * sizeof(HKL));
GetKeyboardLayoutList(layout_count, layouts);
- String ret = _get_full_layout_name_from_registry(layouts[p_index]); // Try reading full name from Windows registry, fallback to locale name if failed (e.g. on Wine).
+ String ret = _get_keyboard_layout_display_name(_get_klid(layouts[p_index])); // Try reading full name from Windows registry, fallback to locale name if failed (e.g. on Wine).
if (ret.is_empty()) {
WCHAR buf[LOCALE_NAME_MAX_LENGTH];
memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(WCHAR));
@@ -2975,6 +3156,21 @@ void DisplayServerWindows::process_events() {
_process_key_events();
Input::get_singleton()->flush_buffered_events();
}
+
+ LocalVector<List<FileDialogData *>::Element *> to_remove;
+ for (List<FileDialogData *>::Element *E = file_dialogs.front(); E; E = E->next()) {
+ FileDialogData *fd = E->get();
+ if (fd->finished.is_set()) {
+ if (fd->listener_thread.is_started()) {
+ fd->listener_thread.wait_to_finish();
+ }
+ to_remove.push_back(E);
+ }
+ }
+ for (List<FileDialogData *>::Element *E : to_remove) {
+ memdelete(E->get());
+ E->erase();
+ }
}
void DisplayServerWindows::force_process_and_drop_events() {
@@ -3807,9 +4003,9 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
case WM_ACTIVATE: {
// Activation can happen just after the window has been created, even before the callbacks are set.
// Therefore, it's safer to defer the delivery of the event.
- if (!windows[window_id].activate_timer_id) {
- windows[window_id].activate_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr);
- }
+ // It's important to set an nIDEvent different from the SetTimer for move_timer_id because
+ // if the same nIDEvent is passed, the timer is replaced and the same timer_id is returned.
+ windows[window_id].activate_timer_id = SetTimer(windows[window_id].hWnd, DisplayServerWindows::TIMER_ID_WINDOW_ACTIVATION, USER_TIMER_MINIMUM, (TIMERPROC) nullptr);
windows[window_id].activate_state = GET_WM_ACTIVATE_STATE(wParam, lParam);
return 0;
} break;
@@ -4160,6 +4356,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
mm->set_relative_screen_position(mm->get_relative());
old_x = mm->get_position().x;
old_y = mm->get_position().y;
+
if (windows[window_id].window_focused || window_get_active_popup() == window_id) {
Input::get_singleton()->parse_input_event(mm);
}
@@ -4186,13 +4383,128 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
break;
}
+ pointer_button[GET_POINTERID_WPARAM(wParam)] = MouseButton::NONE;
windows[window_id].block_mm = true;
return 0;
} break;
case WM_POINTERLEAVE: {
+ pointer_button[GET_POINTERID_WPARAM(wParam)] = MouseButton::NONE;
windows[window_id].block_mm = false;
return 0;
} break;
+ case WM_POINTERDOWN:
+ case WM_POINTERUP: {
+ if (mouse_mode == MOUSE_MODE_CAPTURED && use_raw_input) {
+ break;
+ }
+
+ if ((tablet_get_current_driver() != "winink") || !winink_available) {
+ break;
+ }
+
+ uint32_t pointer_id = LOWORD(wParam);
+ POINTER_INPUT_TYPE pointer_type = PT_POINTER;
+ if (!win8p_GetPointerType(pointer_id, &pointer_type)) {
+ break;
+ }
+
+ if (pointer_type != PT_PEN) {
+ break;
+ }
+
+ Ref<InputEventMouseButton> mb;
+ mb.instantiate();
+ mb->set_window_id(window_id);
+
+ BitField<MouseButtonMask> last_button_state = 0;
+ if (IS_POINTER_FIRSTBUTTON_WPARAM(wParam)) {
+ last_button_state.set_flag(MouseButtonMask::LEFT);
+ mb->set_button_index(MouseButton::LEFT);
+ }
+ if (IS_POINTER_SECONDBUTTON_WPARAM(wParam)) {
+ last_button_state.set_flag(MouseButtonMask::RIGHT);
+ mb->set_button_index(MouseButton::RIGHT);
+ }
+ if (IS_POINTER_THIRDBUTTON_WPARAM(wParam)) {
+ last_button_state.set_flag(MouseButtonMask::MIDDLE);
+ mb->set_button_index(MouseButton::MIDDLE);
+ }
+ if (IS_POINTER_FOURTHBUTTON_WPARAM(wParam)) {
+ last_button_state.set_flag(MouseButtonMask::MB_XBUTTON1);
+ mb->set_button_index(MouseButton::MB_XBUTTON1);
+ }
+ if (IS_POINTER_FIFTHBUTTON_WPARAM(wParam)) {
+ last_button_state.set_flag(MouseButtonMask::MB_XBUTTON2);
+ mb->set_button_index(MouseButton::MB_XBUTTON2);
+ }
+ mb->set_button_mask(last_button_state);
+
+ const BitField<WinKeyModifierMask> &mods = _get_mods();
+ mb->set_ctrl_pressed(mods.has_flag(WinKeyModifierMask::CTRL));
+ mb->set_shift_pressed(mods.has_flag(WinKeyModifierMask::SHIFT));
+ mb->set_alt_pressed(mods.has_flag(WinKeyModifierMask::ALT));
+ mb->set_meta_pressed(mods.has_flag(WinKeyModifierMask::META));
+
+ POINT coords; // Client coords.
+ coords.x = GET_X_LPARAM(lParam);
+ coords.y = GET_Y_LPARAM(lParam);
+
+ // Note: Handle popup closing here, since mouse event is not emulated and hook will not be called.
+ uint64_t delta = OS::get_singleton()->get_ticks_msec() - time_since_popup;
+ if (delta > 250) {
+ Point2i pos = Point2i(coords.x, coords.y) - _get_screens_origin();
+ List<WindowID>::Element *C = nullptr;
+ List<WindowID>::Element *E = popup_list.back();
+ // Find top popup to close.
+ while (E) {
+ // Popup window area.
+ Rect2i win_rect = Rect2i(window_get_position_with_decorations(E->get()), window_get_size_with_decorations(E->get()));
+ // Area of the parent window, which responsible for opening sub-menu.
+ Rect2i safe_rect = window_get_popup_safe_rect(E->get());
+ if (win_rect.has_point(pos)) {
+ break;
+ } else if (safe_rect != Rect2i() && safe_rect.has_point(pos)) {
+ break;
+ } else {
+ C = E;
+ E = E->prev();
+ }
+ }
+ if (C) {
+ _send_window_event(windows[C->get()], DisplayServerWindows::WINDOW_EVENT_CLOSE_REQUEST);
+ }
+ }
+
+ int64_t pen_id = GET_POINTERID_WPARAM(wParam);
+ if (uMsg == WM_POINTERDOWN) {
+ mb->set_pressed(true);
+ if (pointer_down_time.has(pen_id) && (pointer_prev_button[pen_id] == mb->get_button_index()) && (ABS(coords.y - pointer_last_pos[pen_id].y) < GetSystemMetrics(SM_CYDOUBLECLK)) && GetMessageTime() - pointer_down_time[pen_id] < (LONG)GetDoubleClickTime()) {
+ mb->set_double_click(true);
+ pointer_down_time[pen_id] = 0;
+ } else {
+ pointer_down_time[pen_id] = GetMessageTime();
+ pointer_prev_button[pen_id] = mb->get_button_index();
+ pointer_last_pos[pen_id] = Vector2(coords.x, coords.y);
+ }
+ pointer_button[pen_id] = mb->get_button_index();
+ } else {
+ if (!pointer_button.has(pen_id)) {
+ return 0;
+ }
+ mb->set_pressed(false);
+ mb->set_button_index(pointer_button[pen_id]);
+ pointer_button[pen_id] = MouseButton::NONE;
+ }
+
+ ScreenToClient(windows[window_id].hWnd, &coords);
+
+ mb->set_position(Vector2(coords.x, coords.y));
+ mb->set_global_position(Vector2(coords.x, coords.y));
+
+ Input::get_singleton()->parse_input_event(mb);
+
+ return 0;
+ } break;
case WM_POINTERUPDATE: {
if (mouse_mode == MOUSE_MODE_CAPTURED && use_raw_input) {
break;
@@ -4270,7 +4582,23 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
mm->set_alt_pressed(mods.has_flag(WinKeyModifierMask::ALT));
mm->set_meta_pressed(mods.has_flag(WinKeyModifierMask::META));
- mm->set_button_mask(mouse_get_button_state());
+ BitField<MouseButtonMask> last_button_state = 0;
+ if (IS_POINTER_FIRSTBUTTON_WPARAM(wParam)) {
+ last_button_state.set_flag(MouseButtonMask::LEFT);
+ }
+ if (IS_POINTER_SECONDBUTTON_WPARAM(wParam)) {
+ last_button_state.set_flag(MouseButtonMask::RIGHT);
+ }
+ if (IS_POINTER_THIRDBUTTON_WPARAM(wParam)) {
+ last_button_state.set_flag(MouseButtonMask::MIDDLE);
+ }
+ if (IS_POINTER_FOURTHBUTTON_WPARAM(wParam)) {
+ last_button_state.set_flag(MouseButtonMask::MB_XBUTTON1);
+ }
+ if (IS_POINTER_FIFTHBUTTON_WPARAM(wParam)) {
+ last_button_state.set_flag(MouseButtonMask::MB_XBUTTON2);
+ }
+ mm->set_button_mask(last_button_state);
POINT coords; // Client coords.
coords.x = GET_X_LPARAM(lParam);
@@ -4441,6 +4769,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
mm->set_position(mm->get_position() - window_get_position(receiving_window_id) + window_get_position(window_id));
mm->set_global_position(mm->get_position());
}
+
Input::get_singleton()->parse_input_event(mm);
} break;
@@ -4625,6 +4954,16 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
Input::get_singleton()->parse_input_event(mbd);
}
+ // Propagate the button up event to the window on which the button down
+ // event was triggered. This is needed for drag & drop to work between windows,
+ // because the engine expects events to keep being processed
+ // on the same window dragging started.
+ if (mb->is_pressed()) {
+ last_mouse_button_down_window = window_id;
+ } else if (last_mouse_button_down_window != INVALID_WINDOW_ID) {
+ mb->set_window_id(last_mouse_button_down_window);
+ last_mouse_button_down_window = INVALID_WINDOW_ID;
+ }
} break;
case WM_WINDOWPOSCHANGED: {
@@ -4685,16 +5024,16 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
rect_changed = true;
}
#if defined(RD_ENABLED)
- if (rendering_context && window.context_created) {
+ if (window.create_completed && rendering_context && window.context_created) {
// Note: Trigger resize event to update swapchains when window is minimized/restored, even if size is not changed.
rendering_context->window_set_size(window_id, window.width, window.height);
}
#endif
#if defined(GLES3_ENABLED)
- if (gl_manager_native) {
+ if (window.create_completed && gl_manager_native) {
gl_manager_native->window_resize(window_id, window.width, window.height);
}
- if (gl_manager_angle) {
+ if (window.create_completed && gl_manager_angle) {
gl_manager_angle->window_resize(window_id, window.width, window.height);
}
#endif
@@ -4727,7 +5066,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
case WM_ENTERSIZEMOVE: {
Input::get_singleton()->release_pressed_events();
- windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr);
+ windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, DisplayServerWindows::TIMER_ID_MOVE_REDRAW, USER_TIMER_MINIMUM, (TIMERPROC) nullptr);
} break;
case WM_EXITSIZEMOVE: {
KillTimer(windows[window_id].hWnd, windows[window_id].move_timer_id);
@@ -5159,7 +5498,7 @@ void DisplayServerWindows::_update_tablet_ctx(const String &p_old_driver, const
}
}
-DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect) {
+DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect, bool p_exclusive, WindowID p_transient_parent) {
DWORD dwExStyle;
DWORD dwStyle;
@@ -5209,6 +5548,20 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
WindowID id = window_id_counter;
{
+ WindowData *wd_transient_parent = nullptr;
+ HWND owner_hwnd = nullptr;
+ if (p_transient_parent != INVALID_WINDOW_ID) {
+ if (!windows.has(p_transient_parent)) {
+ ERR_PRINT("Condition \"!windows.has(p_transient_parent)\" is true.");
+ p_transient_parent = INVALID_WINDOW_ID;
+ } else {
+ wd_transient_parent = &windows[p_transient_parent];
+ if (p_exclusive) {
+ owner_hwnd = wd_transient_parent->hWnd;
+ }
+ }
+ }
+
WindowData &wd = windows[id];
wd.hWnd = CreateWindowExW(
@@ -5219,7 +5572,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
WindowRect.top,
WindowRect.right - WindowRect.left,
WindowRect.bottom - WindowRect.top,
- nullptr,
+ owner_hwnd,
nullptr,
hInstance,
// tunnel the WindowData we need to handle creation message
@@ -5241,11 +5594,20 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
wd.pre_fs_valid = true;
}
+ wd.exclusive = p_exclusive;
+ if (wd_transient_parent) {
+ wd.transient_parent = p_transient_parent;
+ wd_transient_parent->transient_children.insert(id);
+ }
+
if (is_dark_mode_supported() && dark_title_available) {
BOOL value = is_dark_mode();
::DwmSetWindowAttribute(wd.hWnd, use_legacy_dark_mode_before_20H1 ? DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 : DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));
}
+ RECT real_client_rect;
+ GetClientRect(wd.hWnd, &real_client_rect);
+
#ifdef RD_ENABLED
if (rendering_context) {
union {
@@ -5275,7 +5637,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
return INVALID_WINDOW_ID;
}
- rendering_context->window_set_size(id, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top);
+ rendering_context->window_set_size(id, real_client_rect.right - real_client_rect.left, real_client_rect.bottom - real_client_rect.top);
rendering_context->window_set_vsync_mode(id, p_vsync_mode);
wd.context_created = true;
}
@@ -5283,7 +5645,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
#ifdef GLES3_ENABLED
if (gl_manager_native) {
- if (gl_manager_native->window_create(id, wd.hWnd, hInstance, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top) != OK) {
+ if (gl_manager_native->window_create(id, wd.hWnd, hInstance, real_client_rect.right - real_client_rect.left, real_client_rect.bottom - real_client_rect.top) != OK) {
memdelete(gl_manager_native);
gl_manager_native = nullptr;
windows.erase(id);
@@ -5293,7 +5655,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
}
if (gl_manager_angle) {
- if (gl_manager_angle->window_create(id, nullptr, wd.hWnd, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top) != OK) {
+ if (gl_manager_angle->window_create(id, nullptr, wd.hWnd, real_client_rect.right - real_client_rect.left, real_client_rect.bottom - real_client_rect.top) != OK) {
memdelete(gl_manager_angle);
gl_manager_angle = nullptr;
windows.erase(id);
@@ -5355,7 +5717,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
PROPVARIANT val;
String appname;
if (Engine::get_singleton()->is_editor_hint()) {
- appname = "Godot.GodotEditor." + String(VERSION_BRANCH);
+ appname = "Godot.GodotEditor." + String(VERSION_FULL_CONFIG);
} else {
String name = GLOBAL_GET("application/config/name");
String version = GLOBAL_GET("application/config/version");
@@ -5402,12 +5764,15 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
SetWindowPos(wd.hWnd, HWND_TOP, srect.position.x, srect.position.y, srect.size.width, srect.size.height, SWP_NOZORDER | SWP_NOACTIVATE);
}
+ wd.create_completed = true;
window_id_counter++;
}
return id;
}
+BitField<DisplayServerWindows::DriverID> DisplayServerWindows::tested_drivers = 0;
+
// WinTab API.
bool DisplayServerWindows::wintab_available = false;
WTOpenPtr DisplayServerWindows::wintab_WTOpen = nullptr;
@@ -5432,6 +5797,9 @@ GetPointerPenInfoPtr DisplayServerWindows::win8p_GetPointerPenInfo = nullptr;
LogicalToPhysicalPointForPerMonitorDPIPtr DisplayServerWindows::win81p_LogicalToPhysicalPointForPerMonitorDPI = nullptr;
PhysicalToLogicalPointForPerMonitorDPIPtr DisplayServerWindows::win81p_PhysicalToLogicalPointForPerMonitorDPI = nullptr;
+// Shell API,
+SHLoadIndirectStringPtr DisplayServerWindows::load_indirect_string = nullptr;
+
Vector2i _get_device_ids(const String &p_device_name) {
if (p_device_name.is_empty()) {
return Vector2i();
@@ -5494,12 +5862,6 @@ Vector2i _get_device_ids(const String &p_device_name) {
return ids;
}
-typedef enum _SHC_PROCESS_DPI_AWARENESS {
- SHC_PROCESS_DPI_UNAWARE = 0,
- SHC_PROCESS_SYSTEM_DPI_AWARE = 1,
- SHC_PROCESS_PER_MONITOR_DPI_AWARE = 2
-} SHC_PROCESS_DPI_AWARENESS;
-
bool DisplayServerWindows::is_dark_mode_supported() const {
return ux_theme_available;
}
@@ -5567,6 +5929,8 @@ void DisplayServerWindows::tablet_set_current_driver(const String &p_driver) {
DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error) {
KeyMappingWindows::initialize();
+ tested_drivers.clear();
+
drop_events = false;
key_event_pos = 0;
@@ -5605,6 +5969,12 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win
FreeLibrary(nt_lib);
}
+ // Load Shell API.
+ HMODULE shellapi_lib = LoadLibraryW(L"shlwapi.dll");
+ if (shellapi_lib) {
+ load_indirect_string = (SHLoadIndirectStringPtr)GetProcAddress(shellapi_lib, "SHLoadIndirectString");
+ }
+
// Load UXTheme, available on Windows 10+ only.
if (os_ver.dwBuildNumber >= 10240) {
HMODULE ux_theme_lib = LoadLibraryW(L"uxtheme.dll");
@@ -5729,7 +6099,6 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win
wc.lpszClassName = L"Engine";
if (!RegisterClassExW(&wc)) {
- MessageBoxW(nullptr, L"Failed To Register The Window Class.", L"ERROR", MB_OK | MB_ICONEXCLAMATION);
r_error = ERR_UNAVAILABLE;
return;
}
@@ -5740,37 +6109,89 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win
#if defined(VULKAN_ENABLED)
if (rendering_driver == "vulkan") {
rendering_context = memnew(RenderingContextDriverVulkanWindows);
+ tested_drivers.set_flag(DRIVER_ID_RD_VULKAN);
}
#endif
#if defined(D3D12_ENABLED)
if (rendering_driver == "d3d12") {
rendering_context = memnew(RenderingContextDriverD3D12);
+ tested_drivers.set_flag(DRIVER_ID_RD_D3D12);
}
#endif
if (rendering_context) {
if (rendering_context->initialize() != OK) {
- memdelete(rendering_context);
- rendering_context = nullptr;
- r_error = ERR_UNAVAILABLE;
- return;
+ bool failed = true;
+#if defined(VULKAN_ENABLED)
+ bool fallback_to_vulkan = GLOBAL_GET("rendering/rendering_device/fallback_to_vulkan");
+ if (failed && fallback_to_vulkan && rendering_driver != "vulkan") {
+ memdelete(rendering_context);
+ rendering_context = memnew(RenderingContextDriverVulkanWindows);
+ tested_drivers.set_flag(DRIVER_ID_RD_VULKAN);
+ if (rendering_context->initialize() == OK) {
+ WARN_PRINT("Your video card drivers seem not to support Direct3D 12, switching to Vulkan.");
+ rendering_driver = "vulkan";
+ failed = false;
+ }
+ }
+#endif
+#if defined(D3D12_ENABLED)
+ bool fallback_to_d3d12 = GLOBAL_GET("rendering/rendering_device/fallback_to_d3d12");
+ if (failed && fallback_to_d3d12 && rendering_driver != "d3d12") {
+ memdelete(rendering_context);
+ rendering_context = memnew(RenderingContextDriverD3D12);
+ tested_drivers.set_flag(DRIVER_ID_RD_D3D12);
+ if (rendering_context->initialize() == OK) {
+ WARN_PRINT("Your video card drivers seem not to support Vulkan, switching to Direct3D 12.");
+ rendering_driver = "d3d12";
+ failed = false;
+ }
+ }
+#endif
+ if (failed) {
+ memdelete(rendering_context);
+ rendering_context = nullptr;
+ r_error = ERR_UNAVAILABLE;
+ return;
+ }
}
}
#endif
// Init context and rendering device
#if defined(GLES3_ENABLED)
-#if defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) || defined(_M_ARM64)
- // There's no native OpenGL drivers on Windows for ARM, switch to ANGLE over DX.
+ bool fallback = GLOBAL_GET("rendering/gl_compatibility/fallback_to_angle");
+ bool show_warning = true;
+
if (rendering_driver == "opengl3") {
- rendering_driver = "opengl3_angle";
+ // There's no native OpenGL drivers on Windows for ARM, always enable fallback.
+#if defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) || defined(_M_ARM64)
+ fallback = true;
+ show_warning = false;
+#else
+ typedef BOOL(WINAPI * IsWow64Process2Ptr)(HANDLE, USHORT *, USHORT *);
+
+ IsWow64Process2Ptr IsWow64Process2 = (IsWow64Process2Ptr)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "IsWow64Process2");
+ if (IsWow64Process2) {
+ USHORT process_arch = 0;
+ USHORT machine_arch = 0;
+ if (!IsWow64Process2(GetCurrentProcess(), &process_arch, &machine_arch)) {
+ machine_arch = 0;
+ }
+ if (machine_arch == 0xAA64) {
+ fallback = true;
+ show_warning = false;
+ }
+ }
+#endif
}
-#elif defined(EGL_STATIC)
- bool fallback = GLOBAL_GET("rendering/gl_compatibility/fallback_to_angle");
+
+ bool gl_supported = true;
if (fallback && (rendering_driver == "opengl3")) {
Dictionary gl_info = detect_wgl();
bool force_angle = false;
+ gl_supported = gl_info["version"].operator int() >= 30003;
Vector2i device_id = _get_device_ids(gl_info["name"]);
Array device_list = GLOBAL_GET("rendering/gl_compatibility/force_angle_on_devices");
@@ -5792,41 +6213,61 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win
}
if (force_angle || (gl_info["version"].operator int() < 30003)) {
- WARN_PRINT("Your video card drivers seem not to support the required OpenGL 3.3 version, switching to ANGLE.");
+ tested_drivers.set_flag(DRIVER_ID_COMPAT_OPENGL3);
+ if (show_warning) {
+ if (gl_info["version"].operator int() < 30003) {
+ WARN_PRINT("Your video card drivers seem not to support the required OpenGL 3.3 version, switching to ANGLE.");
+ } else {
+ WARN_PRINT("Your video card drivers are known to have low quality OpenGL 3.3 support, switching to ANGLE.");
+ }
+ }
rendering_driver = "opengl3_angle";
}
}
-#endif
+ if (rendering_driver == "opengl3_angle") {
+ gl_manager_angle = memnew(GLManagerANGLE_Windows);
+ tested_drivers.set_flag(DRIVER_ID_COMPAT_ANGLE_D3D11);
+
+ if (gl_manager_angle->initialize() != OK) {
+ memdelete(gl_manager_angle);
+ gl_manager_angle = nullptr;
+ bool fallback_to_native = GLOBAL_GET("rendering/gl_compatibility/fallback_to_native");
+ if (fallback_to_native && gl_supported) {
+#ifdef EGL_STATIC
+ WARN_PRINT("Your video card drivers seem not to support GLES3 / ANGLE, switching to native OpenGL.");
+#else
+ WARN_PRINT("Your video card drivers seem not to support GLES3 / ANGLE or ANGLE dynamic libraries (libEGL.dll and libGLESv2.dll) are missing, switching to native OpenGL.");
+#endif
+ rendering_driver = "opengl3";
+ } else {
+ r_error = ERR_UNAVAILABLE;
+ ERR_FAIL_MSG("Could not initialize ANGLE OpenGL.");
+ }
+ }
+ }
if (rendering_driver == "opengl3") {
gl_manager_native = memnew(GLManagerNative_Windows);
+ tested_drivers.set_flag(DRIVER_ID_COMPAT_OPENGL3);
if (gl_manager_native->initialize() != OK) {
memdelete(gl_manager_native);
gl_manager_native = nullptr;
r_error = ERR_UNAVAILABLE;
- return;
+ ERR_FAIL_MSG("Could not initialize native OpenGL.");
}
+ }
+ if (rendering_driver == "opengl3") {
RasterizerGLES3::make_current(true);
}
if (rendering_driver == "opengl3_angle") {
- gl_manager_angle = memnew(GLManagerANGLE_Windows);
-
- if (gl_manager_angle->initialize() != OK) {
- memdelete(gl_manager_angle);
- gl_manager_angle = nullptr;
- r_error = ERR_UNAVAILABLE;
- return;
- }
-
RasterizerGLES3::make_current(false);
}
#endif
-
String appname;
if (Engine::get_singleton()->is_editor_hint()) {
- appname = "Godot.GodotEditor." + String(VERSION_BRANCH);
+ appname = "Godot.GodotEditor." + String(VERSION_FULL_CONFIG);
} else {
String name = GLOBAL_GET("application/config/name");
String version = GLOBAL_GET("application/config/version");
@@ -5841,6 +6282,17 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win
}
clean_app_name = clean_app_name.substr(0, 120 - version.length()).trim_suffix(".");
appname = "Godot." + clean_app_name + "." + version;
+
+#ifndef TOOLS_ENABLED
+ // Set for exported projects only.
+ HKEY key;
+ if (RegOpenKeyW(HKEY_CURRENT_USER_LOCAL_SETTINGS, L"Software\\Microsoft\\Windows\\Shell\\MuiCache", &key) == ERROR_SUCCESS) {
+ Char16String cs_name = name.utf16();
+ String value_name = OS::get_singleton()->get_executable_path().replace("/", "\\") + ".FriendlyAppName";
+ RegSetValueExW(key, (LPCWSTR)value_name.utf16().get_data(), 0, REG_SZ, (const BYTE *)cs_name.get_data(), cs_name.size() * sizeof(WCHAR));
+ RegCloseKey(key);
+ }
+#endif
}
SetCurrentProcessExplicitAppUserModelID((PCWSTR)appname.utf16().get_data());
@@ -5857,7 +6309,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win
window_position = scr_rect.position + (scr_rect.size - p_resolution) / 2;
}
- WindowID main_window = _create_window(p_mode, p_vsync_mode, p_flags, Rect2i(window_position, p_resolution));
+ WindowID main_window = _create_window(p_mode, p_vsync_mode, p_flags, Rect2i(window_position, p_resolution), false, INVALID_WINDOW_ID);
ERR_FAIL_COND_MSG(main_window == INVALID_WINDOW_ID, "Failed to create main window.");
joypad = new JoypadWindows(&windows[MAIN_WINDOW_ID].hWnd);
@@ -5934,32 +6386,41 @@ Vector<String> DisplayServerWindows::get_rendering_drivers_func() {
DisplayServer *DisplayServerWindows::create_func(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error) {
DisplayServer *ds = memnew(DisplayServerWindows(p_rendering_driver, p_mode, p_vsync_mode, p_flags, p_position, p_resolution, p_screen, p_context, r_error));
if (r_error != OK) {
- if (p_rendering_driver == "vulkan") {
- String executable_name = OS::get_singleton()->get_executable_path().get_file();
- OS::get_singleton()->alert(
- vformat("Your video card drivers seem not to support the required Vulkan version.\n\n"
- "If possible, consider updating your video card drivers or using the OpenGL 3 driver.\n\n"
- "You can enable the OpenGL 3 driver by starting the engine from the\n"
- "command line with the command:\n\n \"%s\" --rendering-driver opengl3\n\n"
- "If you have recently updated your video card drivers, try rebooting.",
- executable_name),
- "Unable to initialize Vulkan video driver");
- } else if (p_rendering_driver == "d3d12") {
+ if (tested_drivers == 0) {
+ OS::get_singleton()->alert("Failed to register the window class.", "Unable to initialize DisplayServer");
+ } else if (tested_drivers.has_flag(DRIVER_ID_RD_VULKAN) || tested_drivers.has_flag(DRIVER_ID_RD_D3D12)) {
+ Vector<String> drivers;
+ if (tested_drivers.has_flag(DRIVER_ID_RD_VULKAN)) {
+ drivers.push_back("Vulkan");
+ }
+ if (tested_drivers.has_flag(DRIVER_ID_RD_D3D12)) {
+ drivers.push_back("Direct3D 12");
+ }
String executable_name = OS::get_singleton()->get_executable_path().get_file();
OS::get_singleton()->alert(
- vformat("Your video card drivers seem not to support the required DirectX 12 version.\n\n"
+ vformat("Your video card drivers seem not to support the required %s version.\n\n"
"If possible, consider updating your video card drivers or using the OpenGL 3 driver.\n\n"
"You can enable the OpenGL 3 driver by starting the engine from the\n"
"command line with the command:\n\n \"%s\" --rendering-driver opengl3\n\n"
"If you have recently updated your video card drivers, try rebooting.",
+ String(" or ").join(drivers),
executable_name),
- "Unable to initialize DirectX 12 video driver");
+ "Unable to initialize video driver");
} else {
+ Vector<String> drivers;
+ if (tested_drivers.has_flag(DRIVER_ID_COMPAT_OPENGL3)) {
+ drivers.push_back("OpenGL 3.3");
+ }
+ if (tested_drivers.has_flag(DRIVER_ID_COMPAT_ANGLE_D3D11)) {
+ drivers.push_back("Direct3D 11");
+ }
OS::get_singleton()->alert(
- "Your video card drivers seem not to support the required OpenGL 3.3 version.\n\n"
- "If possible, consider updating your video card drivers.\n\n"
- "If you have recently updated your video card drivers, try rebooting.",
- "Unable to initialize OpenGL video driver");
+ vformat(
+ "Your video card drivers seem not to support the required %s version.\n\n"
+ "If possible, consider updating your video card drivers.\n\n"
+ "If you have recently updated your video card drivers, try rebooting.",
+ String(" or ").join(drivers)),
+ "Unable to initialize video driver");
}
}
return ds;
@@ -5970,6 +6431,20 @@ void DisplayServerWindows::register_windows_driver() {
}
DisplayServerWindows::~DisplayServerWindows() {
+ LocalVector<List<FileDialogData *>::Element *> to_remove;
+ for (List<FileDialogData *>::Element *E = file_dialogs.front(); E; E = E->next()) {
+ FileDialogData *fd = E->get();
+ if (fd->listener_thread.is_started()) {
+ fd->close_requested.set();
+ fd->listener_thread.wait_to_finish();
+ }
+ to_remove.push_back(E);
+ }
+ for (List<FileDialogData *>::Element *E : to_remove) {
+ memdelete(E->get());
+ E->erase();
+ }
+
delete joypad;
touch_state.clear();
diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h
index 382f18c239..3deb7ac8b0 100644
--- a/platform/windows/display_server_windows.h
+++ b/platform/windows/display_server_windows.h
@@ -207,6 +207,50 @@ typedef UINT32 PEN_MASK;
#define POINTER_MESSAGE_FLAG_FIRSTBUTTON 0x00000010
#endif
+#ifndef POINTER_MESSAGE_FLAG_SECONDBUTTON
+#define POINTER_MESSAGE_FLAG_SECONDBUTTON 0x00000020
+#endif
+
+#ifndef POINTER_MESSAGE_FLAG_THIRDBUTTON
+#define POINTER_MESSAGE_FLAG_THIRDBUTTON 0x00000040
+#endif
+
+#ifndef POINTER_MESSAGE_FLAG_FOURTHBUTTON
+#define POINTER_MESSAGE_FLAG_FOURTHBUTTON 0x00000080
+#endif
+
+#ifndef POINTER_MESSAGE_FLAG_FIFTHBUTTON
+#define POINTER_MESSAGE_FLAG_FIFTHBUTTON 0x00000100
+#endif
+
+#ifndef IS_POINTER_FLAG_SET_WPARAM
+#define IS_POINTER_FLAG_SET_WPARAM(wParam, flag) (((DWORD)HIWORD(wParam) & (flag)) == (flag))
+#endif
+
+#ifndef IS_POINTER_FIRSTBUTTON_WPARAM
+#define IS_POINTER_FIRSTBUTTON_WPARAM(wParam) IS_POINTER_FLAG_SET_WPARAM(wParam, POINTER_MESSAGE_FLAG_FIRSTBUTTON)
+#endif
+
+#ifndef IS_POINTER_SECONDBUTTON_WPARAM
+#define IS_POINTER_SECONDBUTTON_WPARAM(wParam) IS_POINTER_FLAG_SET_WPARAM(wParam, POINTER_MESSAGE_FLAG_SECONDBUTTON)
+#endif
+
+#ifndef IS_POINTER_THIRDBUTTON_WPARAM
+#define IS_POINTER_THIRDBUTTON_WPARAM(wParam) IS_POINTER_FLAG_SET_WPARAM(wParam, POINTER_MESSAGE_FLAG_THIRDBUTTON)
+#endif
+
+#ifndef IS_POINTER_FOURTHBUTTON_WPARAM
+#define IS_POINTER_FOURTHBUTTON_WPARAM(wParam) IS_POINTER_FLAG_SET_WPARAM(wParam, POINTER_MESSAGE_FLAG_FOURTHBUTTON)
+#endif
+
+#ifndef IS_POINTER_FIFTHBUTTON_WPARAM
+#define IS_POINTER_FIFTHBUTTON_WPARAM(wParam) IS_POINTER_FLAG_SET_WPARAM(wParam, POINTER_MESSAGE_FLAG_FIFTHBUTTON)
+#endif
+
+#ifndef GET_POINTERID_WPARAM
+#define GET_POINTERID_WPARAM(wParam) (LOWORD(wParam))
+#endif
+
#if WINVER < 0x0602
enum tagPOINTER_INPUT_TYPE {
PT_POINTER = 0x00000001,
@@ -274,10 +318,19 @@ typedef struct tagPOINTER_PEN_INFO {
#define WM_POINTERLEAVE 0x024A
#endif
+#ifndef WM_POINTERDOWN
+#define WM_POINTERDOWN 0x0246
+#endif
+
+#ifndef WM_POINTERUP
+#define WM_POINTERUP 0x0247
+#endif
+
typedef BOOL(WINAPI *GetPointerTypePtr)(uint32_t p_id, POINTER_INPUT_TYPE *p_type);
typedef BOOL(WINAPI *GetPointerPenInfoPtr)(uint32_t p_id, POINTER_PEN_INFO *p_pen_info);
typedef BOOL(WINAPI *LogicalToPhysicalPointForPerMonitorDPIPtr)(HWND hwnd, LPPOINT lpPoint);
typedef BOOL(WINAPI *PhysicalToLogicalPointForPerMonitorDPIPtr)(HWND hwnd, LPPOINT lpPoint);
+typedef HRESULT(WINAPI *SHLoadIndirectStringPtr)(PCWSTR pszSource, PWSTR pszOutBuf, UINT cchOutBuf, void **ppvReserved);
typedef struct {
BYTE bWidth; // Width, in pixels, of the image
@@ -297,6 +350,12 @@ typedef struct {
ICONDIRENTRY idEntries[1]; // An entry for each image (idCount of 'em)
} ICONDIR, *LPICONDIR;
+typedef enum _SHC_PROCESS_DPI_AWARENESS {
+ SHC_PROCESS_DPI_UNAWARE = 0,
+ SHC_PROCESS_SYSTEM_DPI_AWARE = 1,
+ SHC_PROCESS_PER_MONITOR_DPI_AWARE = 2,
+} SHC_PROCESS_DPI_AWARENESS;
+
class DisplayServerWindows : public DisplayServer {
// No need to register with GDCLASS, it's platform-specific and nothing is added.
@@ -328,10 +387,26 @@ class DisplayServerWindows : public DisplayServer {
static LogicalToPhysicalPointForPerMonitorDPIPtr win81p_LogicalToPhysicalPointForPerMonitorDPI;
static PhysicalToLogicalPointForPerMonitorDPIPtr win81p_PhysicalToLogicalPointForPerMonitorDPI;
+ // Shell API
+ static SHLoadIndirectStringPtr load_indirect_string;
+
void _update_tablet_ctx(const String &p_old_driver, const String &p_new_driver);
String tablet_driver;
Vector<String> tablet_drivers;
+ enum DriverID {
+ DRIVER_ID_COMPAT_OPENGL3 = 1 << 0,
+ DRIVER_ID_COMPAT_ANGLE_D3D11 = 1 << 1,
+ DRIVER_ID_RD_VULKAN = 1 << 2,
+ DRIVER_ID_RD_D3D12 = 1 << 3,
+ };
+ static BitField<DriverID> tested_drivers;
+
+ enum TimerID {
+ TIMER_ID_MOVE_REDRAW = 1,
+ TIMER_ID_WINDOW_ACTIVATION = 2,
+ };
+
enum {
KEY_EVENT_BUFFER_SIZE = 512
};
@@ -380,6 +455,7 @@ class DisplayServerWindows : public DisplayServer {
Vector<Vector2> mpath;
+ bool create_completed = false;
bool pre_fs_valid = false;
RECT pre_fs_rect;
bool maximized = false;
@@ -456,12 +532,12 @@ class DisplayServerWindows : public DisplayServer {
uint64_t time_since_popup = 0;
Ref<Image> icon;
- WindowID _create_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect);
+ WindowID _create_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect, bool p_exclusive, WindowID p_transient_parent);
WindowID window_id_counter = MAIN_WINDOW_ID;
RBMap<WindowID, WindowData> windows;
WindowID last_focused_window = INVALID_WINDOW_ID;
-
+ WindowID last_mouse_button_down_window = INVALID_WINDOW_ID;
HCURSOR hCursor;
WNDPROC user_proc = nullptr;
@@ -474,6 +550,36 @@ class DisplayServerWindows : public DisplayServer {
IndicatorID indicator_id_counter = 0;
HashMap<IndicatorID, IndicatorData> indicators;
+ struct FileDialogData {
+ HWND hwnd_owner = 0;
+ Rect2i wrect;
+ String appid;
+ String title;
+ String current_directory;
+ String root;
+ String filename;
+ bool show_hidden = false;
+ DisplayServer::FileDialogMode mode = FileDialogMode::FILE_DIALOG_MODE_OPEN_ANY;
+ Vector<String> filters;
+ TypedArray<Dictionary> options;
+ WindowID window_id = DisplayServer::INVALID_WINDOW_ID;
+ Callable callback;
+ bool options_in_cb = false;
+ Thread listener_thread;
+ SafeFlag close_requested;
+ SafeFlag finished;
+ };
+ Mutex file_dialog_mutex;
+ List<FileDialogData *> file_dialogs;
+ HashMap<HWND, FileDialogData *> file_dialog_wnd;
+
+ static void _thread_fd_monitor(void *p_ud);
+
+ HashMap<int64_t, MouseButton> pointer_prev_button;
+ HashMap<int64_t, MouseButton> pointer_button;
+ HashMap<int64_t, LONG> pointer_down_time;
+ HashMap<int64_t, Vector2> pointer_last_pos;
+
void _send_window_event(const WindowData &wd, WindowEvent p_event);
void _get_window_style(bool p_main_window, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_maximized, bool p_maximized_fs, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex);
@@ -526,7 +632,11 @@ class DisplayServerWindows : public DisplayServer {
Error _file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb);
+ String _get_keyboard_layout_display_name(const String &p_klid) const;
+ String _get_klid(HKL p_hkl) const;
+
public:
+ LRESULT WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT MouseProc(int code, WPARAM wParam, LPARAM lParam);
@@ -583,7 +693,7 @@ public:
virtual Vector<DisplayServer::WindowID> get_window_list() const override;
- virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()) override;
+ virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i(), bool p_exclusive = false, WindowID p_transient_parent = INVALID_WINDOW_ID) override;
virtual void show_window(WindowID p_window) override;
virtual void delete_sub_window(WindowID p_window) override;
diff --git a/platform/windows/doc_classes/EditorExportPlatformWindows.xml b/platform/windows/doc_classes/EditorExportPlatformWindows.xml
index 06b272c10e..9e2db756ce 100644
--- a/platform/windows/doc_classes/EditorExportPlatformWindows.xml
+++ b/platform/windows/doc_classes/EditorExportPlatformWindows.xml
@@ -26,7 +26,7 @@
If set to [code]1[/code], ANGLE libraries are exported with the exported application. If set to [code]0[/code], ANGLE libraries are exported only if [member ProjectSettings.rendering/gl_compatibility/driver] is set to [code]"opengl3_angle"[/code].
</member>
<member name="application/export_d3d12" type="int" setter="" getter="">
- If set to [code]1[/code], Direct3D 12 runtime (DXIL, Agility SDK, PIX) libraries are exported with the exported application. If set to [code]0[/code], Direct3D 12 libraries are exported only if [member ProjectSettings.rendering/rendering_device/driver] is set to [code]"d3d12"[/code].
+ If set to [code]1[/code], the Direct3D 12 runtime libraries (Agility SDK, PIX) are exported with the exported application. If set to [code]0[/code], Direct3D 12 libraries are exported only if [member ProjectSettings.rendering/rendering_device/driver] is set to [code]"d3d12"[/code].
</member>
<member name="application/file_description" type="String" setter="" getter="">
File description to be presented to users. Required. See [url=https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block]StringFileInfo[/url].
diff --git a/platform/windows/export/export_plugin.cpp b/platform/windows/export/export_plugin.cpp
index 6ce9d27dc5..b465bd4ecd 100644
--- a/platform/windows/export/export_plugin.cpp
+++ b/platform/windows/export/export_plugin.cpp
@@ -187,6 +187,12 @@ Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset>
template_path = template_path.strip_edges();
if (template_path.is_empty()) {
template_path = find_export_template(get_template_file_name(p_debug ? "debug" : "release", arch));
+ } else {
+ String exe_arch = _get_exe_arch(template_path);
+ if (arch != exe_arch) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Mismatching custom export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch));
+ return ERR_CANT_CREATE;
+ }
}
int export_angle = p_preset->get("application/export_angle");
@@ -208,18 +214,14 @@ Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset>
int export_d3d12 = p_preset->get("application/export_d3d12");
bool agility_sdk_multiarch = p_preset->get("application/d3d12_agility_sdk_multiarch");
- bool include_dxil_libs = false;
+ bool include_d3d12_extra_libs = false;
if (export_d3d12 == 0) {
- include_dxil_libs = (String(GLOBAL_GET("rendering/rendering_device/driver.windows")) == "d3d12") && (String(GLOBAL_GET("rendering/renderer/rendering_method")) != "gl_compatibility");
+ include_d3d12_extra_libs = (String(GLOBAL_GET("rendering/rendering_device/driver.windows")) == "d3d12") && (String(GLOBAL_GET("rendering/renderer/rendering_method")) != "gl_compatibility");
} else if (export_d3d12 == 1) {
- include_dxil_libs = true;
+ include_d3d12_extra_libs = true;
}
- if (include_dxil_libs) {
+ if (include_d3d12_extra_libs) {
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- if (da->file_exists(template_path.get_base_dir().path_join("dxil." + arch + ".dll"))) {
- da->make_dir_recursive(p_path.get_base_dir().path_join(arch));
- da->copy(template_path.get_base_dir().path_join("dxil." + arch + ".dll"), p_path.get_base_dir().path_join(arch).path_join("dxil.dll"), get_chmod_flags());
- }
if (da->file_exists(template_path.get_base_dir().path_join("D3D12Core." + arch + ".dll"))) {
if (agility_sdk_multiarch) {
da->make_dir_recursive(p_path.get_base_dir().path_join(arch));
@@ -757,9 +759,26 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p
}
bool EditorExportPlatformWindows::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
- String err = "";
+ String err;
bool valid = EditorExportPlatformPC::has_valid_export_configuration(p_preset, err, r_missing_templates, p_debug);
+ String custom_debug = p_preset->get("custom_template/debug").operator String().strip_edges();
+ String custom_release = p_preset->get("custom_template/release").operator String().strip_edges();
+ String arch = p_preset->get("binary_format/architecture");
+
+ if (!custom_debug.is_empty() && FileAccess::exists(custom_debug)) {
+ String exe_arch = _get_exe_arch(custom_debug);
+ if (arch != exe_arch) {
+ err += vformat(TTR("Mismatching custom debug export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch) + "\n";
+ }
+ }
+ if (!custom_release.is_empty() && FileAccess::exists(custom_release)) {
+ String exe_arch = _get_exe_arch(custom_release);
+ if (arch != exe_arch) {
+ err += vformat(TTR("Mismatching custom release export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch) + "\n";
+ }
+ }
+
String rcedit_path = EDITOR_GET("export/windows/rcedit");
if (p_preset->get("application/modify_resources") && rcedit_path.is_empty()) {
err += TTR("The rcedit tool must be configured in the Editor Settings (Export > Windows > rcedit) to change the icon or app information data.") + "\n";
@@ -773,7 +792,7 @@ bool EditorExportPlatformWindows::has_valid_export_configuration(const Ref<Edito
}
bool EditorExportPlatformWindows::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
- String err = "";
+ String err;
bool valid = true;
List<ExportOption> options;
@@ -797,6 +816,43 @@ bool EditorExportPlatformWindows::has_valid_project_configuration(const Ref<Edit
return valid;
}
+String EditorExportPlatformWindows::_get_exe_arch(const String &p_path) const {
+ Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
+ if (f.is_null()) {
+ return "invalid";
+ }
+
+ // Jump to the PE header and check the magic number.
+ {
+ f->seek(0x3c);
+ uint32_t pe_pos = f->get_32();
+
+ f->seek(pe_pos);
+ uint32_t magic = f->get_32();
+ if (magic != 0x00004550) {
+ return "invalid";
+ }
+ }
+
+ // Process header.
+ uint16_t machine = f->get_16();
+ f->close();
+
+ switch (machine) {
+ case 0x014c:
+ return "x86_32";
+ case 0x8664:
+ return "x86_64";
+ case 0x01c0:
+ case 0x01c4:
+ return "arm32";
+ case 0xaa64:
+ return "arm64";
+ default:
+ return "unknown";
+ }
+}
+
Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) {
// Patch the header of the "pck" section in the PE file so that it corresponds to the embedded data
diff --git a/platform/windows/export/export_plugin.h b/platform/windows/export/export_plugin.h
index c644b1f9e1..6ccb4a15a7 100644
--- a/platform/windows/export/export_plugin.h
+++ b/platform/windows/export/export_plugin.h
@@ -73,6 +73,8 @@ class EditorExportPlatformWindows : public EditorExportPlatformPC {
Error _rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path, bool p_console_icon);
Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path);
+ String _get_exe_arch(const String &p_path) const;
+
public:
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override;
virtual Error modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) override;
diff --git a/platform/windows/gl_manager_windows_angle.cpp b/platform/windows/gl_manager_windows_angle.cpp
index 3086edc7f2..c52564676f 100644
--- a/platform/windows/gl_manager_windows_angle.cpp
+++ b/platform/windows/gl_manager_windows_angle.cpp
@@ -67,4 +67,9 @@ Vector<EGLint> GLManagerANGLE_Windows::_get_platform_context_attribs() const {
return ret;
}
+void GLManagerANGLE_Windows::window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height) {
+ window_make_current(p_window_id);
+ eglWaitNative(EGL_CORE_NATIVE_ENGINE);
+}
+
#endif // WINDOWS_ENABLED && GLES3_ENABLED
diff --git a/platform/windows/gl_manager_windows_angle.h b/platform/windows/gl_manager_windows_angle.h
index d8dc651cfd..f43a6fbe02 100644
--- a/platform/windows/gl_manager_windows_angle.h
+++ b/platform/windows/gl_manager_windows_angle.h
@@ -50,7 +50,7 @@ private:
virtual Vector<EGLint> _get_platform_context_attribs() const override;
public:
- void window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height) {}
+ void window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height);
GLManagerANGLE_Windows(){};
~GLManagerANGLE_Windows(){};
diff --git a/platform/windows/gl_manager_windows_native.cpp b/platform/windows/gl_manager_windows_native.cpp
index c8d7534e26..8590c46d12 100644
--- a/platform/windows/gl_manager_windows_native.cpp
+++ b/platform/windows/gl_manager_windows_native.cpp
@@ -76,6 +76,8 @@ static String format_error_message(DWORD id) {
const int OGL_THREAD_CONTROL_ID = 0x20C1221E;
const int OGL_THREAD_CONTROL_DISABLE = 0x00000002;
const int OGL_THREAD_CONTROL_ENABLE = 0x00000001;
+const int VRR_MODE_ID = 0x1194F158;
+const int VRR_MODE_FULLSCREEN_ONLY = 0x1;
typedef int(__cdecl *NvAPI_Initialize_t)();
typedef int(__cdecl *NvAPI_Unload_t)();
@@ -104,10 +106,12 @@ static bool nvapi_err_check(const char *msg, int status) {
return true;
}
-// On windows we have to disable threaded optimization when using NVIDIA graphics cards
-// to avoid stuttering, see https://stackoverflow.com/questions/36959508/nvidia-graphics-driver-causing-noticeable-frame-stuttering/37632948
-// also see https://github.com/Ryujinx/Ryujinx/blob/master/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs
-void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() {
+// On windows we have to customize the NVIDIA application profile:
+// * disable threaded optimization when using NVIDIA cards to avoid stuttering, see
+// https://stackoverflow.com/questions/36959508/nvidia-graphics-driver-causing-noticeable-frame-stuttering/37632948
+// https://github.com/Ryujinx/Ryujinx/blob/master/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs
+// * disable G-SYNC in windowed mode, as it results in unstable editor refresh rates
+void GLManagerNative_Windows::_nvapi_setup_profile() {
HMODULE nvapi = nullptr;
#ifdef _WIN64
nvapi = LoadLibraryA("nvapi64.dll");
@@ -239,21 +243,29 @@ void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() {
}
}
- NVDRS_SETTING setting;
- setting.version = NVDRS_SETTING_VER;
- setting.settingId = OGL_THREAD_CONTROL_ID;
- setting.settingType = NVDRS_DWORD_TYPE;
- setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION;
- setting.isCurrentPredefined = 0;
- setting.isPredefinedValid = 0;
+ NVDRS_SETTING ogl_thread_control_setting = {};
+ ogl_thread_control_setting.version = NVDRS_SETTING_VER;
+ ogl_thread_control_setting.settingId = OGL_THREAD_CONTROL_ID;
+ ogl_thread_control_setting.settingType = NVDRS_DWORD_TYPE;
int thread_control_val = OGL_THREAD_CONTROL_DISABLE;
if (!GLOBAL_GET("rendering/gl_compatibility/nvidia_disable_threaded_optimization")) {
thread_control_val = OGL_THREAD_CONTROL_ENABLE;
}
- setting.u32CurrentValue = thread_control_val;
- setting.u32PredefinedValue = thread_control_val;
+ ogl_thread_control_setting.u32CurrentValue = thread_control_val;
- if (!nvapi_err_check("NVAPI: Error calling NvAPI_DRS_SetSetting", NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting))) {
+ if (!nvapi_err_check("NVAPI: Error calling NvAPI_DRS_SetSetting", NvAPI_DRS_SetSetting(session_handle, profile_handle, &ogl_thread_control_setting))) {
+ NvAPI_DRS_DestroySession(session_handle);
+ NvAPI_Unload();
+ return;
+ }
+
+ NVDRS_SETTING vrr_mode_setting = {};
+ vrr_mode_setting.version = NVDRS_SETTING_VER;
+ vrr_mode_setting.settingId = VRR_MODE_ID;
+ vrr_mode_setting.settingType = NVDRS_DWORD_TYPE;
+ vrr_mode_setting.u32CurrentValue = VRR_MODE_FULLSCREEN_ONLY;
+
+ if (!nvapi_err_check("NVAPI: Error calling NvAPI_DRS_SetSetting", NvAPI_DRS_SetSetting(session_handle, profile_handle, &vrr_mode_setting))) {
NvAPI_DRS_DestroySession(session_handle);
NvAPI_Unload();
return;
@@ -270,6 +282,7 @@ void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() {
} else {
print_verbose("NVAPI: Enabled OpenGL threaded optimization successfully");
}
+ print_verbose("NVAPI: Disabled G-SYNC for windowed mode successfully");
NvAPI_DRS_DestroySession(session_handle);
}
@@ -495,7 +508,7 @@ void GLManagerNative_Windows::swap_buffers() {
}
Error GLManagerNative_Windows::initialize() {
- _nvapi_disable_threaded_optimization();
+ _nvapi_setup_profile();
return OK;
}
diff --git a/platform/windows/gl_manager_windows_native.h b/platform/windows/gl_manager_windows_native.h
index b4e2a3acdf..532092ae74 100644
--- a/platform/windows/gl_manager_windows_native.h
+++ b/platform/windows/gl_manager_windows_native.h
@@ -78,7 +78,7 @@ private:
int glx_minor, glx_major;
private:
- void _nvapi_disable_threaded_optimization();
+ void _nvapi_setup_profile();
int _find_or_create_display(GLWindow &win);
Error _create_context(GLWindow &win, GLDisplay &gl_display);
diff --git a/platform/windows/native_menu_windows.cpp b/platform/windows/native_menu_windows.cpp
index d9dc28e9d9..fde55918e4 100644
--- a/platform/windows/native_menu_windows.cpp
+++ b/platform/windows/native_menu_windows.cpp
@@ -81,22 +81,6 @@ void NativeMenuWindows::_menu_activate(HMENU p_menu, int p_index) const {
if (GetMenuItemInfoW(md->menu, p_index, true, &item)) {
MenuItemData *item_data = (MenuItemData *)item.dwItemData;
if (item_data) {
- if (item_data->max_states > 0) {
- item_data->state++;
- if (item_data->state >= item_data->max_states) {
- item_data->state = 0;
- }
- }
-
- if (item_data->checkable_type == CHECKABLE_TYPE_CHECK_BOX) {
- if ((item.fState & MFS_CHECKED) == MFS_CHECKED) {
- item.fState &= ~MFS_CHECKED;
- } else {
- item.fState |= MFS_CHECKED;
- }
- SetMenuItemInfoW(md->menu, p_index, true, &item);
- }
-
if (item_data->callback.is_valid()) {
Variant ret;
Callable::CallError ce;
@@ -619,9 +603,12 @@ bool NativeMenuWindows::is_item_checked(const RID &p_rid, int p_idx) const {
MENUITEMINFOW item;
ZeroMemory(&item, sizeof(item));
item.cbSize = sizeof(item);
- item.fMask = MIIM_STATE;
+ item.fMask = MIIM_STATE | MIIM_DATA;
if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) {
- return (item.fState & MFS_CHECKED) == MFS_CHECKED;
+ MenuItemData *item_data = (MenuItemData *)item.dwItemData;
+ if (item_data) {
+ return item_data->checked;
+ }
}
return false;
}
@@ -861,12 +848,16 @@ void NativeMenuWindows::set_item_checked(const RID &p_rid, int p_idx, bool p_che
MENUITEMINFOW item;
ZeroMemory(&item, sizeof(item));
item.cbSize = sizeof(item);
- item.fMask = MIIM_STATE;
+ item.fMask = MIIM_STATE | MIIM_DATA;
if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) {
- if (p_checked) {
- item.fState |= MFS_CHECKED;
- } else {
- item.fState &= ~MFS_CHECKED;
+ MenuItemData *item_data = (MenuItemData *)item.dwItemData;
+ if (item_data) {
+ item_data->checked = p_checked;
+ if (p_checked) {
+ item.fState |= MFS_CHECKED;
+ } else {
+ item.fState &= ~MFS_CHECKED;
+ }
}
SetMenuItemInfoW(md->menu, p_idx, true, &item);
}
diff --git a/platform/windows/native_menu_windows.h b/platform/windows/native_menu_windows.h
index 5c4aaa52c8..235a4b332a 100644
--- a/platform/windows/native_menu_windows.h
+++ b/platform/windows/native_menu_windows.h
@@ -51,6 +51,7 @@ class NativeMenuWindows : public NativeMenu {
Callable callback;
Variant meta;
GlobalMenuCheckType checkable_type;
+ bool checked = false;
int max_states = 0;
int state = 0;
Ref<Image> img;
diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp
index 157702655e..7316992b60 100644
--- a/platform/windows/os_windows.cpp
+++ b/platform/windows/os_windows.cpp
@@ -166,15 +166,9 @@ void OS_Windows::initialize_debugging() {
static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) {
String err_str;
if (p_errorexp && p_errorexp[0]) {
- err_str = String::utf8(p_errorexp);
+ err_str = String::utf8(p_errorexp) + "\n";
} else {
- err_str = String::utf8(p_file) + ":" + itos(p_line) + " - " + String::utf8(p_error);
- }
-
- if (p_editor_notify) {
- err_str += " (User)\n";
- } else {
- err_str += "\n";
+ err_str = String::utf8(p_file) + ":" + itos(p_line) + " - " + String::utf8(p_error) + "\n";
}
OutputDebugStringW((LPCWSTR)err_str.utf16().ptr());
@@ -379,6 +373,8 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha
//this code exists so gdextension can load .dll files from within the executable path
path = get_executable_path().get_base_dir().path_join(p_path.get_file());
}
+ // Path to load from may be different from original if we make copies.
+ String load_path = path;
ERR_FAIL_COND_V(!FileAccess::exists(path), ERR_FILE_NOT_FOUND);
@@ -387,25 +383,22 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha
if (p_data != nullptr && p_data->generate_temp_files) {
// Copy the file to the same directory as the original with a prefix in the name.
// This is so relative path to dependencies are satisfied.
- String copy_path = path.get_base_dir().path_join("~" + path.get_file());
+ load_path = path.get_base_dir().path_join("~" + path.get_file());
// If there's a left-over copy (possibly from a crash) then delete it first.
- if (FileAccess::exists(copy_path)) {
- DirAccess::remove_absolute(copy_path);
+ if (FileAccess::exists(load_path)) {
+ DirAccess::remove_absolute(load_path);
}
- Error copy_err = DirAccess::copy_absolute(path, copy_path);
+ Error copy_err = DirAccess::copy_absolute(path, load_path);
if (copy_err) {
ERR_PRINT("Error copying library: " + path);
return ERR_CANT_CREATE;
}
- FileAccess::set_hidden_attribute(copy_path, true);
-
- // Save the copied path so it can be deleted later.
- path = copy_path;
+ FileAccess::set_hidden_attribute(load_path, true);
- Error pdb_err = WindowsUtils::copy_and_rename_pdb(path);
+ Error pdb_err = WindowsUtils::copy_and_rename_pdb(load_path);
if (pdb_err != OK && pdb_err != ERR_SKIP) {
WARN_PRINT(vformat("Failed to rename the PDB file. The original PDB file for '%s' will be loaded.", path));
}
@@ -421,21 +414,21 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha
DLL_DIRECTORY_COOKIE cookie = nullptr;
if (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) {
- cookie = add_dll_directory((LPCWSTR)(path.get_base_dir().utf16().get_data()));
+ cookie = add_dll_directory((LPCWSTR)(load_path.get_base_dir().utf16().get_data()));
}
- p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(path.utf16().get_data()), nullptr, (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0);
+ p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(load_path.utf16().get_data()), nullptr, (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0);
if (!p_library_handle) {
if (p_data != nullptr && p_data->generate_temp_files) {
- DirAccess::remove_absolute(path);
+ DirAccess::remove_absolute(load_path);
}
#ifdef DEBUG_ENABLED
DWORD err_code = GetLastError();
- HashSet<String> checekd_libs;
+ HashSet<String> checked_libs;
HashSet<String> missing_libs;
- debug_dynamic_library_check_dependencies(path, path, checekd_libs, missing_libs);
+ debug_dynamic_library_check_dependencies(load_path, load_path, checked_libs, missing_libs);
if (!missing_libs.is_empty()) {
String missing;
for (const String &E : missing_libs) {
@@ -464,7 +457,8 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha
}
if (p_data != nullptr && p_data->generate_temp_files) {
- temp_libraries[p_library_handle] = path;
+ // Save the copied path so it can be deleted later.
+ temp_libraries[p_library_handle] = load_path;
}
return OK;
@@ -622,6 +616,72 @@ Vector<String> OS_Windows::get_video_adapter_driver_info() const {
return info;
}
+bool OS_Windows::get_user_prefers_integrated_gpu() const {
+ // On Windows 10, the preferred GPU configured in Windows Settings is
+ // stored in the registry under the key
+ // `HKEY_CURRENT_USER\SOFTWARE\Microsoft\DirectX\UserGpuPreferences`
+ // with the name being the app ID or EXE path. The value is in the form of
+ // `GpuPreference=1;`, with the value being 1 for integrated GPU and 2
+ // for discrete GPU. On Windows 11, there may be more flags, separated
+ // by semicolons.
+
+ // If this is a packaged app, use the "application user model ID".
+ // Otherwise, use the EXE path.
+ WCHAR value_name[32768];
+ bool is_packaged = false;
+ {
+ HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
+ if (kernel32) {
+ using GetCurrentApplicationUserModelIdPtr = LONG(WINAPI *)(UINT32 * length, PWSTR id);
+ GetCurrentApplicationUserModelIdPtr GetCurrentApplicationUserModelId = (GetCurrentApplicationUserModelIdPtr)GetProcAddress(kernel32, "GetCurrentApplicationUserModelId");
+
+ if (GetCurrentApplicationUserModelId) {
+ UINT32 length = sizeof(value_name) / sizeof(value_name[0]);
+ LONG result = GetCurrentApplicationUserModelId(&length, value_name);
+ if (result == ERROR_SUCCESS) {
+ is_packaged = true;
+ }
+ }
+ }
+ }
+ if (!is_packaged && GetModuleFileNameW(nullptr, value_name, sizeof(value_name) / sizeof(value_name[0])) >= sizeof(value_name) / sizeof(value_name[0])) {
+ // Paths should never be longer than 32767, but just in case.
+ return false;
+ }
+
+ LPCWSTR subkey = L"SOFTWARE\\Microsoft\\DirectX\\UserGpuPreferences";
+ HKEY hkey = nullptr;
+ LSTATUS result = RegOpenKeyExW(HKEY_CURRENT_USER, subkey, 0, KEY_READ, &hkey);
+ if (result != ERROR_SUCCESS) {
+ return false;
+ }
+
+ DWORD size = 0;
+ result = RegGetValueW(hkey, nullptr, value_name, RRF_RT_REG_SZ, nullptr, nullptr, &size);
+ if (result != ERROR_SUCCESS || size == 0) {
+ RegCloseKey(hkey);
+ return false;
+ }
+
+ Vector<WCHAR> buffer;
+ buffer.resize(size / sizeof(WCHAR));
+ result = RegGetValueW(hkey, nullptr, value_name, RRF_RT_REG_SZ, nullptr, (LPBYTE)buffer.ptrw(), &size);
+ if (result != ERROR_SUCCESS) {
+ RegCloseKey(hkey);
+ return false;
+ }
+
+ RegCloseKey(hkey);
+ const String flags = String::utf16((const char16_t *)buffer.ptr(), size / sizeof(WCHAR));
+
+ for (const String &flag : flags.split(";", false)) {
+ if (flag == "GpuPreference=1") {
+ return true;
+ }
+ }
+ return false;
+}
+
OS::DateTime OS_Windows::get_datetime(bool p_utc) const {
SYSTEMTIME systemtime;
if (p_utc) {
@@ -1634,26 +1694,6 @@ String OS_Windows::get_locale() const {
return "en";
}
-// We need this because GetSystemInfo() is unreliable on WOW64
-// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms724381(v=vs.85).aspx
-// Taken from MSDN
-typedef BOOL(WINAPI *LPFN_ISWOW64PROCESS)(HANDLE, PBOOL);
-LPFN_ISWOW64PROCESS fnIsWow64Process;
-
-BOOL is_wow64() {
- BOOL wow64 = FALSE;
-
- fnIsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "IsWow64Process");
-
- if (fnIsWow64Process) {
- if (!fnIsWow64Process(GetCurrentProcess(), &wow64)) {
- wow64 = FALSE;
- }
- }
-
- return wow64;
-}
-
String OS_Windows::get_processor_name() const {
const String id = "Hardware\\Description\\System\\CentralProcessor\\0";
diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h
index b6a21ed42d..9c7b98d7fd 100644
--- a/platform/windows/os_windows.h
+++ b/platform/windows/os_windows.h
@@ -172,6 +172,7 @@ public:
virtual String get_version() const override;
virtual Vector<String> get_video_adapter_driver_info() const override;
+ virtual bool get_user_prefers_integrated_gpu() const override;
virtual void initialize_joypads() override {}
diff --git a/platform/windows/rendering_context_driver_vulkan_windows.cpp b/platform/windows/rendering_context_driver_vulkan_windows.cpp
index f968ffc1d7..445388af89 100644
--- a/platform/windows/rendering_context_driver_vulkan_windows.cpp
+++ b/platform/windows/rendering_context_driver_vulkan_windows.cpp
@@ -64,7 +64,7 @@ RenderingContextDriver::SurfaceID RenderingContextDriverVulkanWindows::surface_c
create_info.hwnd = wpd->window;
VkSurfaceKHR vk_surface = VK_NULL_HANDLE;
- VkResult err = vkCreateWin32SurfaceKHR(instance_get(), &create_info, nullptr, &vk_surface);
+ VkResult err = vkCreateWin32SurfaceKHR(instance_get(), &create_info, get_allocation_callbacks(VK_OBJECT_TYPE_SURFACE_KHR), &vk_surface);
ERR_FAIL_COND_V(err != VK_SUCCESS, SurfaceID());
Surface *surface = memnew(Surface);