1.New Apksigner (to deal with large apk file)
2.Compat for Galaxy Store Stardew Valley
This commit is contained in:
parent
9953510220
commit
20ec03caa2
|
@ -10,8 +10,8 @@ android {
|
||||||
applicationId "com.zane.smapiinstaller"
|
applicationId "com.zane.smapiinstaller"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 20
|
versionCode 21
|
||||||
versionName "1.3.5"
|
versionName "1.3.6"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
|
@ -138,6 +138,7 @@
|
||||||
-keep class com.zane.** { *; }
|
-keep class com.zane.** { *; }
|
||||||
-keep class pxb.android.** { *; }
|
-keep class pxb.android.** { *; }
|
||||||
-keep class net.fornwall.apksigner.** { *; }
|
-keep class net.fornwall.apksigner.** { *; }
|
||||||
|
-keep class com.android.apksig.** { *; }
|
||||||
-keep class org.spongycastle.**
|
-keep class org.spongycastle.**
|
||||||
-dontwarn org.spongycastle.jce.provider.X509LDAPCertStoreSpi
|
-dontwarn org.spongycastle.jce.provider.X509LDAPCertStoreSpi
|
||||||
-dontwarn org.spongycastle.x509.util.LDAPStoreHelper
|
-dontwarn org.spongycastle.x509.util.LDAPStoreHelper
|
||||||
|
|
Binary file not shown.
|
@ -2,6 +2,11 @@
|
||||||
"minBuildCode": 138,
|
"minBuildCode": 138,
|
||||||
"maxBuildCode": null,
|
"maxBuildCode": null,
|
||||||
"basePath": "",
|
"basePath": "",
|
||||||
|
"targetPackageName": [
|
||||||
|
"com.chucklefish.stardewvalley",
|
||||||
|
"com.zane.stardewvalley",
|
||||||
|
"com.martyrpher.stardewvalley"
|
||||||
|
],
|
||||||
"manifestEntries": [
|
"manifestEntries": [
|
||||||
{
|
{
|
||||||
"targetPath": "classes.dex",
|
"targetPath": "classes.dex",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": 5,
|
"version": 6,
|
||||||
"contents": [
|
"contents": [
|
||||||
{
|
{
|
||||||
"type": "COMPAT",
|
"type": "COMPAT",
|
||||||
|
@ -9,6 +9,14 @@
|
||||||
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
|
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
|
||||||
"hash": "bd16e8e4cb52d636e24c6a2d2309b66a60e492d2b97c1b8f6a519c04ac42ebdc"
|
"hash": "bd16e8e4cb52d636e24c6a2d2309b66a60e492d2b97c1b8f6a519c04ac42ebdc"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "COMPAT",
|
||||||
|
"name": "SMAPI for Galaxy Store",
|
||||||
|
"assetPath": "compat/samsung_138/",
|
||||||
|
"description": "SMAPI compat package for game 1.4.4.138 - latest, SMAPI 3.4.0",
|
||||||
|
"url": "http://zaneyork.cn/download/compat/smapi_samsung_138.zip",
|
||||||
|
"hash": "4028512fb1ae048557b63495de6f76cc454510d488a76d35eb1330ef021ec4ec"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "LOCALE",
|
"type": "LOCALE",
|
||||||
"name": "Chinese Locale v2.5.1",
|
"name": "Chinese Locale v2.5.1",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": 5,
|
"version": 6,
|
||||||
"contents": [
|
"contents": [
|
||||||
{
|
{
|
||||||
"type": "COMPAT",
|
"type": "COMPAT",
|
||||||
|
@ -9,6 +9,14 @@
|
||||||
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
|
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
|
||||||
"hash": "bd16e8e4cb52d636e24c6a2d2309b66a60e492d2b97c1b8f6a519c04ac42ebdc"
|
"hash": "bd16e8e4cb52d636e24c6a2d2309b66a60e492d2b97c1b8f6a519c04ac42ebdc"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "COMPAT",
|
||||||
|
"name": "SMAPI for Galaxy Store",
|
||||||
|
"assetPath": "compat/samsung_138/",
|
||||||
|
"description": "SMAPI compat package for game 1.4.4.138 - latest, SMAPI 3.4.0",
|
||||||
|
"url": "http://zaneyork.cn/download/compat/smapi_samsung_138.zip",
|
||||||
|
"hash": "4028512fb1ae048557b63495de6f76cc454510d488a76d35eb1330ef021ec4ec"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "LOCALE",
|
"type": "LOCALE",
|
||||||
"name": "Chinese Locale v2.5.1",
|
"name": "Chinese Locale v2.5.1",
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
{
|
{
|
||||||
"version": 5,
|
"version": 6,
|
||||||
"contents": [
|
"contents": [
|
||||||
{
|
{
|
||||||
"type": "COMPAT",
|
"type": "COMPAT",
|
||||||
"name": "SMAPI for 1.4.5.137",
|
"name": "SMAPI兼容包 1.4.5.137",
|
||||||
"assetPath": "compat/137/",
|
"assetPath": "compat/137/",
|
||||||
"description": "SMAPI兼容包, 适用版本1.4.4.128 - 1.4.5.137, SMAPI 3.3.2.0",
|
"description": "SMAPI兼容包, 适用版本1.4.4.128 - 1.4.5.137, SMAPI 3.3.2.0",
|
||||||
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
|
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
|
||||||
"hash": "bd16e8e4cb52d636e24c6a2d2309b66a60e492d2b97c1b8f6a519c04ac42ebdc"
|
"hash": "bd16e8e4cb52d636e24c6a2d2309b66a60e492d2b97c1b8f6a519c04ac42ebdc"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "COMPAT",
|
||||||
|
"name": "SMAPI三星商店兼容包",
|
||||||
|
"assetPath": "compat/samsung_138/",
|
||||||
|
"description": "SMAPI三星商店兼容包, 适用版本1.4.4.138至今, SMAPI 3.4.0",
|
||||||
|
"url": "http://zaneyork.cn/download/compat/smapi_samsung_138.zip",
|
||||||
|
"hash": "4028512fb1ae048557b63495de6f76cc454510d488a76d35eb1330ef021ec4ec"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "LOCALE",
|
"type": "LOCALE",
|
||||||
"name": "中文汉化v2.5.1",
|
"name": "中文汉化v2.5.1",
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
"com.chucklefish.stardewvalley",
|
"com.chucklefish.stardewvalley",
|
||||||
"com.chucklefish.stardewvalleysamsung",
|
"com.chucklefish.stardewvalleysamsung",
|
||||||
"com.zane.stardewvalley",
|
"com.zane.stardewvalley",
|
||||||
|
"com.zane.stardewvalleysamsung",
|
||||||
"com.martyrpher.stardewvalley"
|
"com.martyrpher.stardewvalley"
|
||||||
]
|
]
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,520 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig;
|
||||||
|
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.RunnablesExecutor;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.UnsupportedOperationException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK signing logic which is independent of how input and output APKs are stored, parsed, and
|
||||||
|
* generated.
|
||||||
|
*
|
||||||
|
* <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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.android.apksig;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class Hints {
|
||||||
|
/**
|
||||||
|
* Name of hint pattern asset file in APK.
|
||||||
|
*/
|
||||||
|
public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of hint byte range data file in APK. Keep in sync with PinnerService.java.
|
||||||
|
*/
|
||||||
|
public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
|
||||||
|
|
||||||
|
private static int clampToInt(long value) {
|
||||||
|
return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ByteRange {
|
||||||
|
final long start;
|
||||||
|
final long end;
|
||||||
|
|
||||||
|
public ByteRange(long start, long end) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a blob of bytes that PinnerService understands as a
|
||||||
|
* sequence of byte ranges to pin.
|
||||||
|
*/
|
||||||
|
public static byte[] encodeByteRangeList(List<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<Pattern> parsePinPatterns(byte[] patternBlob) {
|
||||||
|
ArrayList<Pattern> pinPatterns = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
|
||||||
|
String line = rawLine.replaceFirst("#.*", ""); // # starts a comment
|
||||||
|
if (!("".equals(line))) {
|
||||||
|
pinPatterns.add(Pattern.compile(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (UnsupportedEncodingException ex) {
|
||||||
|
throw new RuntimeException("UTF-8 must be supported", ex);
|
||||||
|
}
|
||||||
|
return pinPatterns;
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a
|
||||||
|
* well-formed ZIP archive, in which case {@link #getCause()} will return a
|
||||||
|
* {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains
|
||||||
|
* multiple ZIP entries with the same name.
|
||||||
|
*/
|
||||||
|
public class ApkFormatException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public ApkFormatException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApkFormatException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that no APK Signing Block was found in an APK.
|
||||||
|
*/
|
||||||
|
public class ApkSigningBlockNotFoundException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public ApkSigningBlockNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApkSigningBlockNotFoundException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,604 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.apk;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.apk.AndroidBinXmlParser;
|
||||||
|
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.internal.zip.CentralDirectoryRecord;
|
||||||
|
import com.android.apksig.internal.zip.LocalFileRecord;
|
||||||
|
import com.android.apksig.internal.zip.ZipUtils;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.zip.ZipFormatException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK utilities.
|
||||||
|
*/
|
||||||
|
public abstract class ApkUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the Android manifest ZIP entry in APKs.
|
||||||
|
*/
|
||||||
|
public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
|
||||||
|
|
||||||
|
private ApkUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the main ZIP sections of the provided APK.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurred while reading the APK
|
||||||
|
* @throws ZipFormatException if the APK is malformed
|
||||||
|
*/
|
||||||
|
public static ZipSections findZipSections(DataSource apk)
|
||||||
|
throws IOException, ZipFormatException {
|
||||||
|
Pair<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the ZIP sections of an APK.
|
||||||
|
*/
|
||||||
|
public static class ZipSections {
|
||||||
|
private final long mCentralDirectoryOffset;
|
||||||
|
private final long mCentralDirectorySizeBytes;
|
||||||
|
private final int mCentralDirectoryRecordCount;
|
||||||
|
private final long mEocdOffset;
|
||||||
|
private final ByteBuffer mEocd;
|
||||||
|
|
||||||
|
public ZipSections(
|
||||||
|
long centralDirectoryOffset,
|
||||||
|
long centralDirectorySizeBytes,
|
||||||
|
int centralDirectoryRecordCount,
|
||||||
|
long eocdOffset,
|
||||||
|
ByteBuffer eocd) {
|
||||||
|
mCentralDirectoryOffset = centralDirectoryOffset;
|
||||||
|
mCentralDirectorySizeBytes = centralDirectorySizeBytes;
|
||||||
|
mCentralDirectoryRecordCount = centralDirectoryRecordCount;
|
||||||
|
mEocdOffset = eocdOffset;
|
||||||
|
mEocd = eocd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the start offset of the ZIP Central Directory. This value is taken from the
|
||||||
|
* ZIP End of Central Directory record.
|
||||||
|
*/
|
||||||
|
public long getZipCentralDirectoryOffset() {
|
||||||
|
return mCentralDirectoryOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the
|
||||||
|
* ZIP End of Central Directory record.
|
||||||
|
*/
|
||||||
|
public long getZipCentralDirectorySizeBytes() {
|
||||||
|
return mCentralDirectorySizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of records in the ZIP Central Directory. This value is taken from the
|
||||||
|
* ZIP End of Central Directory record.
|
||||||
|
*/
|
||||||
|
public int getZipCentralDirectoryRecordCount() {
|
||||||
|
return mCentralDirectoryRecordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the start offset of the ZIP End of Central Directory record. The record extends
|
||||||
|
* until the very end of the APK.
|
||||||
|
*/
|
||||||
|
public long getZipEndOfCentralDirectoryOffset() {
|
||||||
|
return mEocdOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the contents of the ZIP End of Central Directory.
|
||||||
|
*/
|
||||||
|
public ByteBuffer getZipEndOfCentralDirectory() {
|
||||||
|
return mEocd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
|
||||||
|
* Directory record.
|
||||||
|
*
|
||||||
|
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
|
||||||
|
* @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
|
||||||
|
* be between {@code 0} and {@code 2^32 - 1} inclusive.
|
||||||
|
*/
|
||||||
|
public static void setZipEocdCentralDirectoryOffset(
|
||||||
|
ByteBuffer zipEndOfCentralDirectory, long offset) {
|
||||||
|
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
|
||||||
|
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://source.android.com/security/apksigning/v2.html
|
||||||
|
private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
|
||||||
|
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
|
||||||
|
private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the APK Signing Block of the provided APK.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
|
||||||
|
*
|
||||||
|
* @see <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that there was an issue determining the minimum Android platform version supported by
|
||||||
|
* an APK because the version is specified as a codename, rather than as API Level number, and the
|
||||||
|
* codename is in an unexpected format.
|
||||||
|
*/
|
||||||
|
public class CodenameMinSdkVersionException extends MinSdkVersionException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** Encountered codename. */
|
||||||
|
private final String mCodename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code MinSdkVersionCodenameException} with the provided message and
|
||||||
|
* codename.
|
||||||
|
*/
|
||||||
|
public CodenameMinSdkVersionException(String message, String codename) {
|
||||||
|
super(message);
|
||||||
|
mCodename = codename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the codename.
|
||||||
|
*/
|
||||||
|
public String getCodename() {
|
||||||
|
return mCodename;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that there was an issue determining the minimum Android platform version supported by
|
||||||
|
* an APK.
|
||||||
|
*/
|
||||||
|
public class MinSdkVersionException extends ApkFormatException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code MinSdkVersionException} with the provided message.
|
||||||
|
*/
|
||||||
|
public MinSdkVersionException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code MinSdkVersionException} with the provided message and cause.
|
||||||
|
*/
|
||||||
|
public MinSdkVersionException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,869 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}.
|
||||||
|
*
|
||||||
|
* <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
|
||||||
|
* {@link #getEventType()} and {@link #next()} methods. Additional information about the current
|
||||||
|
* event can be obtained via an assortment of getters, for example, {@link #getName()} or
|
||||||
|
* {@link #getAttributeNameResourceId(int)}.
|
||||||
|
*/
|
||||||
|
public class AndroidBinXmlParser {
|
||||||
|
|
||||||
|
/** Event: start of document. */
|
||||||
|
public static final int EVENT_START_DOCUMENT = 1;
|
||||||
|
|
||||||
|
/** Event: end of document. */
|
||||||
|
public static final int EVENT_END_DOCUMENT = 2;
|
||||||
|
|
||||||
|
/** Event: start of an element. */
|
||||||
|
public static final int EVENT_START_ELEMENT = 3;
|
||||||
|
|
||||||
|
/** Event: end of an document. */
|
||||||
|
public static final int EVENT_END_ELEMENT = 4;
|
||||||
|
|
||||||
|
/** Attribute value type is not supported by this parser. */
|
||||||
|
public static final int VALUE_TYPE_UNSUPPORTED = 0;
|
||||||
|
|
||||||
|
/** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
|
||||||
|
public static final int VALUE_TYPE_STRING = 1;
|
||||||
|
|
||||||
|
/** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
|
||||||
|
public static final int VALUE_TYPE_INT = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
|
||||||
|
*/
|
||||||
|
public static final int VALUE_TYPE_REFERENCE = 3;
|
||||||
|
|
||||||
|
/** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
|
||||||
|
public static final int VALUE_TYPE_BOOLEAN = 4;
|
||||||
|
|
||||||
|
private static final long NO_NAMESPACE = 0xffffffffL;
|
||||||
|
|
||||||
|
private final ByteBuffer mXml;
|
||||||
|
|
||||||
|
private StringPool mStringPool;
|
||||||
|
private ResourceMap mResourceMap;
|
||||||
|
private int mDepth;
|
||||||
|
private int mCurrentEvent = EVENT_START_DOCUMENT;
|
||||||
|
|
||||||
|
private String mCurrentElementName;
|
||||||
|
private String mCurrentElementNamespace;
|
||||||
|
private int mCurrentElementAttributeCount;
|
||||||
|
private List<Attribute> mCurrentElementAttributes;
|
||||||
|
private ByteBuffer mCurrentElementAttributesContents;
|
||||||
|
private int mCurrentElementAttrSizeBytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new parser for the provided document.
|
||||||
|
*/
|
||||||
|
public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
|
||||||
|
xml.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
Chunk resXmlChunk = null;
|
||||||
|
while (xml.hasRemaining()) {
|
||||||
|
Chunk chunk = Chunk.get(xml);
|
||||||
|
if (chunk == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (chunk.getType() == Chunk.TYPE_RES_XML) {
|
||||||
|
resXmlChunk = chunk;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resXmlChunk == null) {
|
||||||
|
throw new XmlParserException("No XML chunk in file");
|
||||||
|
}
|
||||||
|
mXml = resXmlChunk.getContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the depth of the current element. Outside of the root of the document the depth is
|
||||||
|
* {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
|
||||||
|
* is decremented by {@code 1} after each {@code end element} event.
|
||||||
|
*/
|
||||||
|
public int getDepth() {
|
||||||
|
return mDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of the current event. See {@code EVENT_...} constants.
|
||||||
|
*/
|
||||||
|
public int getEventType() {
|
||||||
|
return mCurrentEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the local name of the current element or {@code null} if the current event does not
|
||||||
|
* pertain to an element.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mCurrentElementName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the namespace of the current element or {@code null} if the current event does not
|
||||||
|
* pertain to an element. Returns an empty string if the element is not associated with a
|
||||||
|
* namespace.
|
||||||
|
*/
|
||||||
|
public String getNamespace() {
|
||||||
|
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mCurrentElementNamespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of attributes of the element associated with the current event or
|
||||||
|
* {@code -1} if no element is associated with the current event.
|
||||||
|
*/
|
||||||
|
public int getAttributeCount() {
|
||||||
|
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mCurrentElementAttributeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource ID corresponding to the name of the specified attribute of the current
|
||||||
|
* element or {@code 0} if the name is not associated with a resource ID.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public int getAttributeNameResourceId(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getNameResourceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the specified attribute of the current element.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public String getAttributeName(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the specified attribute of the current element or an empty string if
|
||||||
|
* the attribute is not associated with a namespace.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public String getAttributeNamespace(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getNamespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value type of the specified attribute of the current element. See
|
||||||
|
* {@code VALUE_TYPE_...} constants.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public int getAttributeValueType(int index) throws XmlParserException {
|
||||||
|
int type = getAttribute(index).getValueType();
|
||||||
|
switch (type) {
|
||||||
|
case Attribute.TYPE_STRING:
|
||||||
|
return VALUE_TYPE_STRING;
|
||||||
|
case Attribute.TYPE_INT_DEC:
|
||||||
|
case Attribute.TYPE_INT_HEX:
|
||||||
|
return VALUE_TYPE_INT;
|
||||||
|
case Attribute.TYPE_REFERENCE:
|
||||||
|
return VALUE_TYPE_REFERENCE;
|
||||||
|
case Attribute.TYPE_INT_BOOLEAN:
|
||||||
|
return VALUE_TYPE_BOOLEAN;
|
||||||
|
default:
|
||||||
|
return VALUE_TYPE_UNSUPPORTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the integer value of the specified attribute of the current element. See
|
||||||
|
* {@code VALUE_TYPE_...} constants.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event.
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public int getAttributeIntValue(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getIntValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the boolean value of the specified attribute of the current element. See
|
||||||
|
* {@code VALUE_TYPE_...} constants.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event.
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public boolean getAttributeBooleanValue(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getBooleanValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the string value of the specified attribute of the current element. See
|
||||||
|
* {@code VALUE_TYPE_...} constants.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event.
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public String getAttributeStringValue(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getStringValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Attribute getAttribute(int index) {
|
||||||
|
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||||
|
throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
|
||||||
|
}
|
||||||
|
if (index < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("index must be >= 0");
|
||||||
|
}
|
||||||
|
if (index >= mCurrentElementAttributeCount) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"index must be <= attr count (" + mCurrentElementAttributeCount + ")");
|
||||||
|
}
|
||||||
|
parseCurrentElementAttributesIfNotParsed();
|
||||||
|
return mCurrentElementAttributes.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
|
||||||
|
*/
|
||||||
|
public int next() throws XmlParserException {
|
||||||
|
// Decrement depth if the previous event was "end element".
|
||||||
|
if (mCurrentEvent == EVENT_END_ELEMENT) {
|
||||||
|
mDepth--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read events from document, ignoring events that we don't report to caller. Stop at the
|
||||||
|
// earliest event which we report to caller.
|
||||||
|
while (mXml.hasRemaining()) {
|
||||||
|
Chunk chunk = Chunk.get(mXml);
|
||||||
|
if (chunk == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (chunk.getType()) {
|
||||||
|
case Chunk.TYPE_STRING_POOL:
|
||||||
|
if (mStringPool != null) {
|
||||||
|
throw new XmlParserException("Multiple string pools not supported");
|
||||||
|
}
|
||||||
|
mStringPool = new StringPool(chunk);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Chunk.RES_XML_TYPE_START_ELEMENT:
|
||||||
|
{
|
||||||
|
if (mStringPool == null) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Named element encountered before string pool");
|
||||||
|
}
|
||||||
|
ByteBuffer contents = chunk.getContents();
|
||||||
|
if (contents.remaining() < 20) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Start element chunk too short. Need at least 20 bytes. Available: "
|
||||||
|
+ contents.remaining() + " bytes");
|
||||||
|
}
|
||||||
|
long nsId = getUnsignedInt32(contents);
|
||||||
|
long nameId = getUnsignedInt32(contents);
|
||||||
|
int attrStartOffset = getUnsignedInt16(contents);
|
||||||
|
int attrSizeBytes = getUnsignedInt16(contents);
|
||||||
|
int attrCount = getUnsignedInt16(contents);
|
||||||
|
long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
|
||||||
|
contents.position(0);
|
||||||
|
if (attrStartOffset > contents.remaining()) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Attributes start offset out of bounds: " + attrStartOffset
|
||||||
|
+ ", max: " + contents.remaining());
|
||||||
|
}
|
||||||
|
if (attrEndOffset > contents.remaining()) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Attributes end offset out of bounds: " + attrEndOffset
|
||||||
|
+ ", max: " + contents.remaining());
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentElementName = mStringPool.getString(nameId);
|
||||||
|
mCurrentElementNamespace =
|
||||||
|
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||||
|
mCurrentElementAttributeCount = attrCount;
|
||||||
|
mCurrentElementAttributes = null;
|
||||||
|
mCurrentElementAttrSizeBytes = attrSizeBytes;
|
||||||
|
mCurrentElementAttributesContents =
|
||||||
|
sliceFromTo(contents, attrStartOffset, attrEndOffset);
|
||||||
|
|
||||||
|
mDepth++;
|
||||||
|
mCurrentEvent = EVENT_START_ELEMENT;
|
||||||
|
return mCurrentEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Chunk.RES_XML_TYPE_END_ELEMENT:
|
||||||
|
{
|
||||||
|
if (mStringPool == null) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Named element encountered before string pool");
|
||||||
|
}
|
||||||
|
ByteBuffer contents = chunk.getContents();
|
||||||
|
if (contents.remaining() < 8) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"End element chunk too short. Need at least 8 bytes. Available: "
|
||||||
|
+ contents.remaining() + " bytes");
|
||||||
|
}
|
||||||
|
long nsId = getUnsignedInt32(contents);
|
||||||
|
long nameId = getUnsignedInt32(contents);
|
||||||
|
mCurrentElementName = mStringPool.getString(nameId);
|
||||||
|
mCurrentElementNamespace =
|
||||||
|
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||||
|
mCurrentEvent = EVENT_END_ELEMENT;
|
||||||
|
mCurrentElementAttributes = null;
|
||||||
|
mCurrentElementAttributesContents = null;
|
||||||
|
return mCurrentEvent;
|
||||||
|
}
|
||||||
|
case Chunk.RES_XML_TYPE_RESOURCE_MAP:
|
||||||
|
if (mResourceMap != null) {
|
||||||
|
throw new XmlParserException("Multiple resource maps not supported");
|
||||||
|
}
|
||||||
|
mResourceMap = new ResourceMap(chunk);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unknown chunk type -- ignore
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentEvent = EVENT_END_DOCUMENT;
|
||||||
|
return mCurrentEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseCurrentElementAttributesIfNotParsed() {
|
||||||
|
if (mCurrentElementAttributes != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
|
||||||
|
for (int i = 0; i < mCurrentElementAttributeCount; i++) {
|
||||||
|
int startPosition = i * mCurrentElementAttrSizeBytes;
|
||||||
|
ByteBuffer attr =
|
||||||
|
sliceFromTo(
|
||||||
|
mCurrentElementAttributesContents,
|
||||||
|
startPosition,
|
||||||
|
startPosition + mCurrentElementAttrSizeBytes);
|
||||||
|
long nsId = getUnsignedInt32(attr);
|
||||||
|
long nameId = getUnsignedInt32(attr);
|
||||||
|
attr.position(attr.position() + 7); // skip ignored fields
|
||||||
|
int valueType = getUnsignedInt8(attr);
|
||||||
|
long valueData = getUnsignedInt32(attr);
|
||||||
|
mCurrentElementAttributes.add(
|
||||||
|
new Attribute(
|
||||||
|
nsId,
|
||||||
|
nameId,
|
||||||
|
valueType,
|
||||||
|
(int) valueData,
|
||||||
|
mStringPool,
|
||||||
|
mResourceMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Attribute {
|
||||||
|
private static final int TYPE_REFERENCE = 1;
|
||||||
|
private static final int TYPE_STRING = 3;
|
||||||
|
private static final int TYPE_INT_DEC = 0x10;
|
||||||
|
private static final int TYPE_INT_HEX = 0x11;
|
||||||
|
private static final int TYPE_INT_BOOLEAN = 0x12;
|
||||||
|
|
||||||
|
private final long mNsId;
|
||||||
|
private final long mNameId;
|
||||||
|
private final int mValueType;
|
||||||
|
private final int mValueData;
|
||||||
|
private final StringPool mStringPool;
|
||||||
|
private final ResourceMap mResourceMap;
|
||||||
|
|
||||||
|
private Attribute(
|
||||||
|
long nsId,
|
||||||
|
long nameId,
|
||||||
|
int valueType,
|
||||||
|
int valueData,
|
||||||
|
StringPool stringPool,
|
||||||
|
ResourceMap resourceMap) {
|
||||||
|
mNsId = nsId;
|
||||||
|
mNameId = nameId;
|
||||||
|
mValueType = valueType;
|
||||||
|
mValueData = valueData;
|
||||||
|
mStringPool = stringPool;
|
||||||
|
mResourceMap = resourceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNameResourceId() {
|
||||||
|
return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() throws XmlParserException {
|
||||||
|
return mStringPool.getString(mNameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNamespace() throws XmlParserException {
|
||||||
|
return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getValueType() {
|
||||||
|
return mValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIntValue() throws XmlParserException {
|
||||||
|
switch (mValueType) {
|
||||||
|
case TYPE_REFERENCE:
|
||||||
|
case TYPE_INT_DEC:
|
||||||
|
case TYPE_INT_HEX:
|
||||||
|
case TYPE_INT_BOOLEAN:
|
||||||
|
return mValueData;
|
||||||
|
default:
|
||||||
|
throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getBooleanValue() throws XmlParserException {
|
||||||
|
switch (mValueType) {
|
||||||
|
case TYPE_INT_BOOLEAN:
|
||||||
|
return mValueData != 0;
|
||||||
|
default:
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Cannot coerce to boolean: value type " + mValueType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStringValue() throws XmlParserException {
|
||||||
|
switch (mValueType) {
|
||||||
|
case TYPE_STRING:
|
||||||
|
return mStringPool.getString(mValueData & 0xffffffffL);
|
||||||
|
case TYPE_INT_DEC:
|
||||||
|
return Integer.toString(mValueData);
|
||||||
|
case TYPE_INT_HEX:
|
||||||
|
return "0x" + Integer.toHexString(mValueData);
|
||||||
|
case TYPE_INT_BOOLEAN:
|
||||||
|
return Boolean.toString(mValueData != 0);
|
||||||
|
case TYPE_REFERENCE:
|
||||||
|
return "@" + Integer.toHexString(mValueData);
|
||||||
|
default:
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Cannot coerce to string: value type " + mValueType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
|
||||||
|
* contents.
|
||||||
|
*/
|
||||||
|
private static class Chunk {
|
||||||
|
public static final int TYPE_STRING_POOL = 1;
|
||||||
|
public static final int TYPE_RES_XML = 3;
|
||||||
|
public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
|
||||||
|
public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
|
||||||
|
public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
|
||||||
|
|
||||||
|
static final int HEADER_MIN_SIZE_BYTES = 8;
|
||||||
|
|
||||||
|
private final int mType;
|
||||||
|
private final ByteBuffer mHeader;
|
||||||
|
private final ByteBuffer mContents;
|
||||||
|
|
||||||
|
public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
|
||||||
|
mType = type;
|
||||||
|
mHeader = header;
|
||||||
|
mContents = contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getContents() {
|
||||||
|
ByteBuffer result = mContents.slice();
|
||||||
|
result.order(mContents.order());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getHeader() {
|
||||||
|
ByteBuffer result = mHeader.slice();
|
||||||
|
result.order(mHeader.order());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getType() {
|
||||||
|
return mType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes the chunk located at the current position of the input and returns the chunk
|
||||||
|
* or {@code null} if there is no chunk left in the input.
|
||||||
|
*
|
||||||
|
* @throws XmlParserException if the chunk is malformed
|
||||||
|
*/
|
||||||
|
public static Chunk get(ByteBuffer input) throws XmlParserException {
|
||||||
|
if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
|
||||||
|
// Android ignores the last chunk if its header is too big to fit into the file
|
||||||
|
input.position(input.limit());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int originalPosition = input.position();
|
||||||
|
int type = getUnsignedInt16(input);
|
||||||
|
int headerSize = getUnsignedInt16(input);
|
||||||
|
long chunkSize = getUnsignedInt32(input);
|
||||||
|
long chunkRemaining = chunkSize - 8;
|
||||||
|
if (chunkRemaining > input.remaining()) {
|
||||||
|
// Android ignores the last chunk if it's too big to fit into the file
|
||||||
|
input.position(input.limit());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (headerSize < HEADER_MIN_SIZE_BYTES) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Malformed chunk: header too short: " + headerSize + " bytes");
|
||||||
|
} else if (headerSize > chunkSize) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
|
||||||
|
+ chunkSize + " bytes");
|
||||||
|
}
|
||||||
|
int contentStartPosition = originalPosition + headerSize;
|
||||||
|
long chunkEndPosition = originalPosition + chunkSize;
|
||||||
|
Chunk chunk =
|
||||||
|
new Chunk(
|
||||||
|
type,
|
||||||
|
sliceFromTo(input, originalPosition, contentStartPosition),
|
||||||
|
sliceFromTo(input, contentStartPosition, chunkEndPosition));
|
||||||
|
input.position((int) chunkEndPosition);
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
|
||||||
|
*/
|
||||||
|
private static class StringPool {
|
||||||
|
private static final int FLAG_UTF8 = 1 << 8;
|
||||||
|
|
||||||
|
private final ByteBuffer mChunkContents;
|
||||||
|
private final ByteBuffer mStringsSection;
|
||||||
|
private final int mStringCount;
|
||||||
|
private final boolean mUtf8Encoded;
|
||||||
|
private final Map<Integer, String> mCachedStrings = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new string pool from the provided chunk.
|
||||||
|
*
|
||||||
|
* @throws XmlParserException if a parsing error occurred
|
||||||
|
*/
|
||||||
|
public StringPool(Chunk chunk) throws XmlParserException {
|
||||||
|
ByteBuffer header = chunk.getHeader();
|
||||||
|
int headerSizeBytes = header.remaining();
|
||||||
|
header.position(Chunk.HEADER_MIN_SIZE_BYTES);
|
||||||
|
if (header.remaining() < 20) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"XML chunk's header too short. Required at least 20 bytes. Available: "
|
||||||
|
+ header.remaining() + " bytes");
|
||||||
|
}
|
||||||
|
long stringCount = getUnsignedInt32(header);
|
||||||
|
if (stringCount > Integer.MAX_VALUE) {
|
||||||
|
throw new XmlParserException("Too many strings: " + stringCount);
|
||||||
|
}
|
||||||
|
mStringCount = (int) stringCount;
|
||||||
|
long styleCount = getUnsignedInt32(header);
|
||||||
|
if (styleCount > Integer.MAX_VALUE) {
|
||||||
|
throw new XmlParserException("Too many styles: " + styleCount);
|
||||||
|
}
|
||||||
|
long flags = getUnsignedInt32(header);
|
||||||
|
long stringsStartOffset = getUnsignedInt32(header);
|
||||||
|
long stylesStartOffset = getUnsignedInt32(header);
|
||||||
|
|
||||||
|
ByteBuffer contents = chunk.getContents();
|
||||||
|
if (mStringCount > 0) {
|
||||||
|
int stringsSectionStartOffsetInContents =
|
||||||
|
(int) (stringsStartOffset - headerSizeBytes);
|
||||||
|
int stringsSectionEndOffsetInContents;
|
||||||
|
if (styleCount > 0) {
|
||||||
|
// Styles section follows the strings section
|
||||||
|
if (stylesStartOffset < stringsStartOffset) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Styles offset (" + stylesStartOffset + ") < strings offset ("
|
||||||
|
+ stringsStartOffset + ")");
|
||||||
|
}
|
||||||
|
stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
|
||||||
|
} else {
|
||||||
|
stringsSectionEndOffsetInContents = contents.remaining();
|
||||||
|
}
|
||||||
|
mStringsSection =
|
||||||
|
sliceFromTo(
|
||||||
|
contents,
|
||||||
|
stringsSectionStartOffsetInContents,
|
||||||
|
stringsSectionEndOffsetInContents);
|
||||||
|
} else {
|
||||||
|
mStringsSection = ByteBuffer.allocate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
mUtf8Encoded = (flags & FLAG_UTF8) != 0;
|
||||||
|
mChunkContents = contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the string located at the specified {@code 0}-based index in this pool.
|
||||||
|
*
|
||||||
|
* @throws XmlParserException if the string does not exist or cannot be decoded
|
||||||
|
*/
|
||||||
|
public String getString(long index) throws XmlParserException {
|
||||||
|
if (index < 0) {
|
||||||
|
throw new XmlParserException("Unsuported string index: " + index);
|
||||||
|
} else if (index >= mStringCount) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Unsuported string index: " + index + ", max: " + (mStringCount - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = (int) index;
|
||||||
|
String result = mCachedStrings.get(idx);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
|
||||||
|
if (offsetInStringsSection >= mStringsSection.capacity()) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
|
||||||
|
+ ", max: " + (mStringsSection.capacity() - 1));
|
||||||
|
}
|
||||||
|
mStringsSection.position((int) offsetInStringsSection);
|
||||||
|
result =
|
||||||
|
(mUtf8Encoded)
|
||||||
|
? getLengthPrefixedUtf8EncodedString(mStringsSection)
|
||||||
|
: getLengthPrefixedUtf16EncodedString(mStringsSection);
|
||||||
|
mCachedStrings.put(idx, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
|
||||||
|
throws XmlParserException {
|
||||||
|
// If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
|
||||||
|
// Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
|
||||||
|
// of supported values is 0 to 0x7fffffff inclusive.
|
||||||
|
int lengthChars = getUnsignedInt16(encoded);
|
||||||
|
if ((lengthChars & 0x8000) != 0) {
|
||||||
|
lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
|
||||||
|
}
|
||||||
|
if (lengthChars > Integer.MAX_VALUE / 2) {
|
||||||
|
throw new XmlParserException("String too long: " + lengthChars + " uint16s");
|
||||||
|
}
|
||||||
|
int lengthBytes = lengthChars * 2;
|
||||||
|
|
||||||
|
byte[] arr;
|
||||||
|
int arrOffset;
|
||||||
|
if (encoded.hasArray()) {
|
||||||
|
arr = encoded.array();
|
||||||
|
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||||
|
encoded.position(encoded.position() + lengthBytes);
|
||||||
|
} else {
|
||||||
|
arr = new byte[lengthBytes];
|
||||||
|
arrOffset = 0;
|
||||||
|
encoded.get(arr);
|
||||||
|
}
|
||||||
|
// Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
|
||||||
|
// array of bytes is NULL terminated.
|
||||||
|
if ((arr[arrOffset + lengthBytes] != 0)
|
||||||
|
|| (arr[arrOffset + lengthBytes + 1] != 0)) {
|
||||||
|
throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("UTF-16LE character encoding not supported", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
|
||||||
|
throws XmlParserException {
|
||||||
|
// If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
|
||||||
|
// it is stored as a big-endian uint16 with highest bit set. Thus, the range of
|
||||||
|
// supported values is 0 to 0x7fff inclusive.
|
||||||
|
|
||||||
|
// Skip UTF-16 encoded length (in uint16s)
|
||||||
|
int lengthBytes = getUnsignedInt8(encoded);
|
||||||
|
if ((lengthBytes & 0x80) != 0) {
|
||||||
|
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read UTF-8 encoded length (in bytes)
|
||||||
|
lengthBytes = getUnsignedInt8(encoded);
|
||||||
|
if ((lengthBytes & 0x80) != 0) {
|
||||||
|
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] arr;
|
||||||
|
int arrOffset;
|
||||||
|
if (encoded.hasArray()) {
|
||||||
|
arr = encoded.array();
|
||||||
|
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||||
|
encoded.position(encoded.position() + lengthBytes);
|
||||||
|
} else {
|
||||||
|
arr = new byte[lengthBytes];
|
||||||
|
arrOffset = 0;
|
||||||
|
encoded.get(arr);
|
||||||
|
}
|
||||||
|
// Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
|
||||||
|
// of bytes is NULL terminated.
|
||||||
|
if (arr[arrOffset + lengthBytes] != 0) {
|
||||||
|
throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new String(arr, arrOffset, lengthBytes, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("UTF-8 character encoding not supported", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
|
||||||
|
* map.
|
||||||
|
*/
|
||||||
|
private static class ResourceMap {
|
||||||
|
private final ByteBuffer mChunkContents;
|
||||||
|
private final int mEntryCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new resource map from the provided chunk.
|
||||||
|
*
|
||||||
|
* @throws XmlParserException if a parsing error occurred
|
||||||
|
*/
|
||||||
|
public ResourceMap(Chunk chunk) throws XmlParserException {
|
||||||
|
mChunkContents = chunk.getContents().slice();
|
||||||
|
mChunkContents.order(chunk.getContents().order());
|
||||||
|
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||||
|
mEntryCount = mChunkContents.remaining() / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource ID located at the specified {@code 0}-based index in this pool or
|
||||||
|
* {@code 0} if the index is out of range.
|
||||||
|
*/
|
||||||
|
public int getResourceId(long index) {
|
||||||
|
if ((index < 0) || (index >= mEntryCount)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int idx = (int) index;
|
||||||
|
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||||
|
return mChunkContents.getInt(idx * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||||
|
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||||
|
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||||
|
* buffer's byte order.
|
||||||
|
*/
|
||||||
|
private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
|
||||||
|
if (start < 0) {
|
||||||
|
throw new IllegalArgumentException("start: " + start);
|
||||||
|
}
|
||||||
|
if (end < start) {
|
||||||
|
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||||
|
}
|
||||||
|
int capacity = source.capacity();
|
||||||
|
if (end > source.capacity()) {
|
||||||
|
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||||
|
}
|
||||||
|
return sliceFromTo(source, (int) start, (int) end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||||
|
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||||
|
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||||
|
* buffer's byte order.
|
||||||
|
*/
|
||||||
|
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||||
|
if (start < 0) {
|
||||||
|
throw new IllegalArgumentException("start: " + start);
|
||||||
|
}
|
||||||
|
if (end < start) {
|
||||||
|
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||||
|
}
|
||||||
|
int capacity = source.capacity();
|
||||||
|
if (end > source.capacity()) {
|
||||||
|
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||||
|
}
|
||||||
|
int originalLimit = source.limit();
|
||||||
|
int originalPosition = source.position();
|
||||||
|
try {
|
||||||
|
source.position(0);
|
||||||
|
source.limit(end);
|
||||||
|
source.position(start);
|
||||||
|
ByteBuffer result = source.slice();
|
||||||
|
result.order(source.order());
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
source.position(0);
|
||||||
|
source.limit(originalLimit);
|
||||||
|
source.position(originalPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getUnsignedInt8(ByteBuffer buffer) {
|
||||||
|
return buffer.get() & 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getUnsignedInt16(ByteBuffer buffer) {
|
||||||
|
return buffer.getShort() & 0xffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getUnsignedInt32(ByteBuffer buffer) {
|
||||||
|
return buffer.getInt() & 0xffffffffL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getUnsignedInt32(ByteBuffer buffer, int position) {
|
||||||
|
return buffer.getInt(position) & 0xffffffffL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an error occurred while parsing a document.
|
||||||
|
*/
|
||||||
|
public static class XmlParserException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public XmlParserException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public XmlParserException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme v2 content digest algorithm.
|
||||||
|
*/
|
||||||
|
public enum ContentDigestAlgorithm {
|
||||||
|
/** SHA2-256 over 1 MB chunks. */
|
||||||
|
CHUNKED_SHA256("SHA-256", 256 / 8),
|
||||||
|
|
||||||
|
/** SHA2-512 over 1 MB chunks. */
|
||||||
|
CHUNKED_SHA512("SHA-512", 512 / 8),
|
||||||
|
|
||||||
|
/** SHA2-256 over 4 KB chunks for APK verity. */
|
||||||
|
VERITY_CHUNKED_SHA256("SHA-256", 256 / 8);
|
||||||
|
|
||||||
|
private final String mJcaMessageDigestAlgorithm;
|
||||||
|
private final int mChunkDigestOutputSizeBytes;
|
||||||
|
|
||||||
|
private ContentDigestAlgorithm(
|
||||||
|
String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
|
||||||
|
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
|
||||||
|
mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
|
||||||
|
* chunks by this content digest algorithm.
|
||||||
|
*/
|
||||||
|
String getJcaMessageDigestAlgorithm() {
|
||||||
|
return mJcaMessageDigestAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size (in bytes) of the digest of a chunk of content.
|
||||||
|
*/
|
||||||
|
int getChunkDigestOutputSizeBytes() {
|
||||||
|
return mChunkDigestOutputSizeBytes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.security.spec.MGF1ParameterSpec;
|
||||||
|
import java.security.spec.PSSParameterSpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signing Block signature algorithm.
|
||||||
|
*/
|
||||||
|
public enum SignatureAlgorithm {
|
||||||
|
// TODO reserve the 0x0000 ID to mean null
|
||||||
|
/**
|
||||||
|
* RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
|
||||||
|
* digested using SHA2-256 in 1 MB chunks.
|
||||||
|
*/
|
||||||
|
RSA_PSS_WITH_SHA256(
|
||||||
|
0x0101,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||||
|
"RSA",
|
||||||
|
Pair.of("SHA256withRSA/PSS",
|
||||||
|
new PSSParameterSpec(
|
||||||
|
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
|
||||||
|
AndroidSdkVersion.N),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
|
||||||
|
* digested using SHA2-512 in 1 MB chunks.
|
||||||
|
*/
|
||||||
|
RSA_PSS_WITH_SHA512(
|
||||||
|
0x0102,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||||
|
"RSA",
|
||||||
|
Pair.of(
|
||||||
|
"SHA512withRSA/PSS",
|
||||||
|
new PSSParameterSpec(
|
||||||
|
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
|
||||||
|
AndroidSdkVersion.N),
|
||||||
|
|
||||||
|
/** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||||
|
RSA_PKCS1_V1_5_WITH_SHA256(
|
||||||
|
0x0103,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||||
|
"RSA",
|
||||||
|
Pair.of("SHA256withRSA", null),
|
||||||
|
AndroidSdkVersion.N),
|
||||||
|
|
||||||
|
/** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
||||||
|
RSA_PKCS1_V1_5_WITH_SHA512(
|
||||||
|
0x0104,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||||
|
"RSA",
|
||||||
|
Pair.of("SHA512withRSA", null),
|
||||||
|
AndroidSdkVersion.N),
|
||||||
|
|
||||||
|
/** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||||
|
ECDSA_WITH_SHA256(
|
||||||
|
0x0201,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||||
|
"EC",
|
||||||
|
Pair.of("SHA256withECDSA", null),
|
||||||
|
AndroidSdkVersion.N),
|
||||||
|
|
||||||
|
/** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
||||||
|
ECDSA_WITH_SHA512(
|
||||||
|
0x0202,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||||
|
"EC",
|
||||||
|
Pair.of("SHA512withECDSA", null),
|
||||||
|
AndroidSdkVersion.N),
|
||||||
|
|
||||||
|
/** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||||
|
DSA_WITH_SHA256(
|
||||||
|
0x0301,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||||
|
"DSA",
|
||||||
|
Pair.of("SHA256withDSA", null),
|
||||||
|
AndroidSdkVersion.N),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
|
||||||
|
* the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
|
||||||
|
* in little endian) construct the final digest.
|
||||||
|
*/
|
||||||
|
VERITY_RSA_PKCS1_V1_5_WITH_SHA256(
|
||||||
|
0x0421,
|
||||||
|
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||||
|
"RSA",
|
||||||
|
Pair.of("SHA256withRSA", null),
|
||||||
|
AndroidSdkVersion.P),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
|
||||||
|
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
|
||||||
|
* endian) construct the final digest.
|
||||||
|
*/
|
||||||
|
VERITY_ECDSA_WITH_SHA256(
|
||||||
|
0x0423,
|
||||||
|
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||||
|
"EC",
|
||||||
|
Pair.of("SHA256withECDSA", null),
|
||||||
|
AndroidSdkVersion.P),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
|
||||||
|
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
|
||||||
|
* endian) construct the final digest.
|
||||||
|
*/
|
||||||
|
VERITY_DSA_WITH_SHA256(
|
||||||
|
0x0425,
|
||||||
|
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||||
|
"DSA",
|
||||||
|
Pair.of("SHA256withDSA", null),
|
||||||
|
AndroidSdkVersion.P);
|
||||||
|
|
||||||
|
private final int mId;
|
||||||
|
private final String mJcaKeyAlgorithm;
|
||||||
|
private final ContentDigestAlgorithm mContentDigestAlgorithm;
|
||||||
|
private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
|
||||||
|
private final int mMinSdkVersion;
|
||||||
|
|
||||||
|
SignatureAlgorithm(int id,
|
||||||
|
ContentDigestAlgorithm contentDigestAlgorithm,
|
||||||
|
String jcaKeyAlgorithm,
|
||||||
|
Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
|
||||||
|
int minSdkVersion) {
|
||||||
|
mId = id;
|
||||||
|
mContentDigestAlgorithm = contentDigestAlgorithm;
|
||||||
|
mJcaKeyAlgorithm = jcaKeyAlgorithm;
|
||||||
|
mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
|
||||||
|
*/
|
||||||
|
public int getId() {
|
||||||
|
return mId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content digest algorithm associated with this signature algorithm.
|
||||||
|
*/
|
||||||
|
public ContentDigestAlgorithm getContentDigestAlgorithm() {
|
||||||
|
return mContentDigestAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
|
||||||
|
*/
|
||||||
|
public String getJcaKeyAlgorithm() {
|
||||||
|
return mJcaKeyAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
|
||||||
|
* (or null if not needed) to parameterize the {@code Signature}.
|
||||||
|
*/
|
||||||
|
public Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
|
||||||
|
return mJcaSignatureAlgAndParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinSdkVersion() {
|
||||||
|
return mMinSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignatureAlgorithm findById(int id) {
|
||||||
|
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
|
||||||
|
if (alg.getId() == id) {
|
||||||
|
return alg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme block and additional information relevant to verifying the signatures
|
||||||
|
* contained in the block against the file.
|
||||||
|
*/
|
||||||
|
public class SignatureInfo {
|
||||||
|
/** Contents of APK Signature Scheme block. */
|
||||||
|
public final ByteBuffer signatureBlock;
|
||||||
|
|
||||||
|
/** Position of the APK Signing Block in the file. */
|
||||||
|
public final long apkSigningBlockOffset;
|
||||||
|
|
||||||
|
/** Position of the ZIP Central Directory in the file. */
|
||||||
|
public final long centralDirOffset;
|
||||||
|
|
||||||
|
/** Position of the ZIP End of Central Directory (EoCD) in the file. */
|
||||||
|
public final long eocdOffset;
|
||||||
|
|
||||||
|
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
|
||||||
|
public final ByteBuffer eocd;
|
||||||
|
|
||||||
|
public SignatureInfo(
|
||||||
|
ByteBuffer signatureBlock,
|
||||||
|
long apkSigningBlockOffset,
|
||||||
|
long centralDirOffset,
|
||||||
|
long eocdOffset,
|
||||||
|
ByteBuffer eocd) {
|
||||||
|
this.signatureBlock = signatureBlock;
|
||||||
|
this.apkSigningBlockOffset = apkSigningBlockOffset;
|
||||||
|
this.centralDirOffset = centralDirOffset;
|
||||||
|
this.eocdOffset = eocdOffset;
|
||||||
|
this.eocd = eocd;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk.v1;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digest algorithm used with JAR signing (aka v1 signing scheme).
|
||||||
|
*/
|
||||||
|
public enum DigestAlgorithm {
|
||||||
|
/** SHA-1 */
|
||||||
|
SHA1("SHA-1"),
|
||||||
|
|
||||||
|
/** SHA2-256 */
|
||||||
|
SHA256("SHA-256");
|
||||||
|
|
||||||
|
private final String mJcaMessageDigestAlgorithm;
|
||||||
|
|
||||||
|
private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
|
||||||
|
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link java.security.MessageDigest} algorithm represented by this digest
|
||||||
|
* algorithm.
|
||||||
|
*/
|
||||||
|
String getJcaMessageDigestAlgorithm() {
|
||||||
|
return mJcaMessageDigestAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Comparator<DigestAlgorithm> BY_STRENGTH_COMPARATOR = new StrengthComparator();
|
||||||
|
|
||||||
|
private static class StrengthComparator implements Comparator<DigestAlgorithm> {
|
||||||
|
@Override
|
||||||
|
public int compare(DigestAlgorithm a1, DigestAlgorithm a2) {
|
||||||
|
switch (a1) {
|
||||||
|
case SHA1:
|
||||||
|
switch (a2) {
|
||||||
|
case SHA1:
|
||||||
|
return 0;
|
||||||
|
case SHA256:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Unsupported algorithm: " + a2);
|
||||||
|
|
||||||
|
case SHA256:
|
||||||
|
switch (a2) {
|
||||||
|
case SHA1:
|
||||||
|
return 1;
|
||||||
|
case SHA256:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Unsupported algorithm: " + a2);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Unsupported algorithm: " + a1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,688 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk.v1;
|
||||||
|
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1DerEncoder;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1EncodingException;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerEncoding;
|
||||||
|
import com.android.apksig.internal.jar.ManifestWriter;
|
||||||
|
import com.android.apksig.internal.jar.SignatureFileWriter;
|
||||||
|
import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
|
||||||
|
import com.android.apksig.internal.pkcs7.ContentInfo;
|
||||||
|
import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo;
|
||||||
|
import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
|
||||||
|
import com.android.apksig.internal.pkcs7.Pkcs7Constants;
|
||||||
|
import com.android.apksig.internal.pkcs7.SignedData;
|
||||||
|
import com.android.apksig.internal.pkcs7.SignerIdentifier;
|
||||||
|
import com.android.apksig.internal.pkcs7.SignerInfo;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import javax.security.auth.x500.X500Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK signer which uses JAR signing (aka v1 signing scheme).
|
||||||
|
*
|
||||||
|
* @see <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 = "META-INF/MANIFEST.MF";
|
||||||
|
|
||||||
|
private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
|
||||||
|
new Attributes.Name("Created-By");
|
||||||
|
private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
|
||||||
|
private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
|
||||||
|
|
||||||
|
static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = "X-Android-APK-Signed";
|
||||||
|
private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
|
||||||
|
new Attributes.Name(SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signer configuration.
|
||||||
|
*/
|
||||||
|
public static class SignerConfig {
|
||||||
|
/** Name. */
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
/** Private key. */
|
||||||
|
public PrivateKey privateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certificates, with the first certificate containing the public key corresponding to
|
||||||
|
* {@link #privateKey}.
|
||||||
|
*/
|
||||||
|
public List<X509Certificate> certificates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digest algorithm used for the signature.
|
||||||
|
*/
|
||||||
|
public DigestAlgorithm signatureDigestAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V1SchemeSigner() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the JAR signing digest algorithm to be used for signing an APK using the provided key.
|
||||||
|
*
|
||||||
|
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
||||||
|
* AndroidManifest.xml minSdkVersion attribute)
|
||||||
|
*
|
||||||
|
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using
|
||||||
|
* JAR signing (aka v1 signature scheme)
|
||||||
|
*/
|
||||||
|
public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
|
||||||
|
PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
|
||||||
|
String keyAlgorithm = signingKey.getAlgorithm();
|
||||||
|
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Prior to API Level 18, only SHA-1 can be used with RSA.
|
||||||
|
if (minSdkVersion < 18) {
|
||||||
|
return DigestAlgorithm.SHA1;
|
||||||
|
}
|
||||||
|
return DigestAlgorithm.SHA256;
|
||||||
|
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Prior to API Level 21, only SHA-1 can be used with DSA
|
||||||
|
if (minSdkVersion < 21) {
|
||||||
|
return DigestAlgorithm.SHA1;
|
||||||
|
} else {
|
||||||
|
return DigestAlgorithm.SHA256;
|
||||||
|
}
|
||||||
|
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
if (minSdkVersion < 18) {
|
||||||
|
throw new InvalidKeyException(
|
||||||
|
"ECDSA signatures only supported for minSdkVersion 18 and higher");
|
||||||
|
}
|
||||||
|
return DigestAlgorithm.SHA256;
|
||||||
|
} else {
|
||||||
|
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a safe version of the provided signer name.
|
||||||
|
*/
|
||||||
|
public static String getSafeSignerName(String name) {
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Empty name");
|
||||||
|
}
|
||||||
|
|
||||||
|
// According to https://docs.oracle.com/javase/tutorial/deployment/jar/signing.html, the
|
||||||
|
// name must not be longer than 8 characters and may contain only A-Z, 0-9, _, and -.
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
char[] nameCharsUpperCase = name.toUpperCase(Locale.US).toCharArray();
|
||||||
|
for (int i = 0; i < Math.min(nameCharsUpperCase.length, 8); i++) {
|
||||||
|
char c = nameCharsUpperCase[i];
|
||||||
|
if (((c >= 'A') && (c <= 'Z'))
|
||||||
|
|| ((c >= '0') && (c <= '9'))
|
||||||
|
|| (c == '-')
|
||||||
|
|| (c == '_')) {
|
||||||
|
result.append(c);
|
||||||
|
} else {
|
||||||
|
result.append('_');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
|
||||||
|
*/
|
||||||
|
private static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm)
|
||||||
|
throws NoSuchAlgorithmException {
|
||||||
|
String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
|
||||||
|
return MessageDigest.getInstance(jcaAlgorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest
|
||||||
|
* algorithm.
|
||||||
|
*/
|
||||||
|
public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
|
||||||
|
return digestAlgorithm.getJcaMessageDigestAlgorithm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
|
||||||
|
* manifest.
|
||||||
|
*/
|
||||||
|
public static boolean isJarEntryDigestNeededInManifest(String entryName) {
|
||||||
|
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
|
||||||
|
|
||||||
|
// Entries which represent directories sould not be listed in the manifest.
|
||||||
|
if (entryName.endsWith("/")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entries outside of META-INF must be listed in the manifest.
|
||||||
|
if (!entryName.startsWith("META-INF/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Entries in subdirectories of META-INF must be listed in the manifest.
|
||||||
|
if (entryName.indexOf('/', "META-INF/".length()) != -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignored file names (case-insensitive) in META-INF directory:
|
||||||
|
// MANIFEST.MF
|
||||||
|
// *.SF
|
||||||
|
// *.RSA
|
||||||
|
// *.DSA
|
||||||
|
// *.EC
|
||||||
|
// SIG-*
|
||||||
|
String fileNameLowerCase =
|
||||||
|
entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
|
||||||
|
if (("manifest.mf".equals(fileNameLowerCase))
|
||||||
|
|| (fileNameLowerCase.endsWith(".sf"))
|
||||||
|
|| (fileNameLowerCase.endsWith(".rsa"))
|
||||||
|
|| (fileNameLowerCase.endsWith(".dsa"))
|
||||||
|
|| (fileNameLowerCase.endsWith(".ec"))
|
||||||
|
|| (fileNameLowerCase.startsWith("sig-"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
|
||||||
|
* JAR entries which need to be added to the APK as part of the signature.
|
||||||
|
*
|
||||||
|
* @param signerConfigs signer configurations, one for each signer. At least one signer config
|
||||||
|
* must be provided.
|
||||||
|
*
|
||||||
|
* @throws ApkFormatException if the source manifest is malformed
|
||||||
|
* @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
|
||||||
|
* missing
|
||||||
|
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
||||||
|
* cannot be used in general
|
||||||
|
* @throws SignatureException if an error occurs when computing digests of generating
|
||||||
|
* signatures
|
||||||
|
*/
|
||||||
|
public static List<Pair<String, byte[]>> sign(
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
DigestAlgorithm jarEntryDigestAlgorithm,
|
||||||
|
Map<String, byte[]> jarEntryDigests,
|
||||||
|
List<Integer> apkSigningSchemeIds,
|
||||||
|
byte[] sourceManifestBytes,
|
||||||
|
String createdBy)
|
||||||
|
throws NoSuchAlgorithmException, ApkFormatException, InvalidKeyException,
|
||||||
|
CertificateException, SignatureException {
|
||||||
|
if (signerConfigs.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||||
|
}
|
||||||
|
OutputManifestFile manifest =
|
||||||
|
generateManifestFile(
|
||||||
|
jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
|
||||||
|
|
||||||
|
return signManifest(
|
||||||
|
signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, createdBy, manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
|
||||||
|
* JAR entries which need to be added to the APK as part of the signature.
|
||||||
|
*
|
||||||
|
* @param signerConfigs signer configurations, one for each signer. At least one signer config
|
||||||
|
* must be provided.
|
||||||
|
*
|
||||||
|
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
||||||
|
* cannot be used in general
|
||||||
|
* @throws SignatureException if an error occurs when computing digests of generating
|
||||||
|
* signatures
|
||||||
|
*/
|
||||||
|
public static List<Pair<String, byte[]>> signManifest(
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
DigestAlgorithm digestAlgorithm,
|
||||||
|
List<Integer> apkSigningSchemeIds,
|
||||||
|
String createdBy,
|
||||||
|
OutputManifestFile manifest)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
||||||
|
SignatureException {
|
||||||
|
if (signerConfigs.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
|
||||||
|
List<Pair<String, byte[]>> signatureJarEntries =
|
||||||
|
new ArrayList<>(2 * signerConfigs.size() + 1);
|
||||||
|
byte[] sfBytes =
|
||||||
|
generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, createdBy, manifest);
|
||||||
|
for (SignerConfig signerConfig : signerConfigs) {
|
||||||
|
String signerName = signerConfig.name;
|
||||||
|
byte[] signatureBlock;
|
||||||
|
try {
|
||||||
|
signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException(
|
||||||
|
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new CertificateException(
|
||||||
|
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SignatureException(
|
||||||
|
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||||
|
}
|
||||||
|
signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
|
||||||
|
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||||
|
String signatureBlockFileName =
|
||||||
|
"META-INF/" + signerName + "."
|
||||||
|
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
|
||||||
|
signatureJarEntries.add(
|
||||||
|
Pair.of(signatureBlockFileName, signatureBlock));
|
||||||
|
}
|
||||||
|
signatureJarEntries.add(Pair.of(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(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ASN.1 DER-encoded {@code NULL}. */
|
||||||
|
private static final Asn1OpaqueObject ASN1_DER_NULL =
|
||||||
|
new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the CMS PKCS #7 signature block corresponding to the provided signature file and
|
||||||
|
* signing configuration.
|
||||||
|
*/
|
||||||
|
private static byte[] generateSignatureBlock(
|
||||||
|
SignerConfig signerConfig, byte[] signatureFileBytes)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
||||||
|
SignatureException {
|
||||||
|
// Obtain relevant bits of signing configuration
|
||||||
|
List<X509Certificate> signerCerts = signerConfig.certificates;
|
||||||
|
X509Certificate signingCert = signerCerts.get(0);
|
||||||
|
PublicKey publicKey = signingCert.getPublicKey();
|
||||||
|
DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
|
||||||
|
Pair<String, AlgorithmIdentifier> signatureAlgs =
|
||||||
|
getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm);
|
||||||
|
String jcaSignatureAlgorithm = signatureAlgs.getFirst();
|
||||||
|
|
||||||
|
// Generate the cryptographic signature of the signature file
|
||||||
|
byte[] signatureBytes;
|
||||||
|
try {
|
||||||
|
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
signature.initSign(signerConfig.privateKey);
|
||||||
|
signature.update(signatureFileBytes);
|
||||||
|
signatureBytes = signature.sign();
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the signature against the public key in the signing certificate
|
||||||
|
try {
|
||||||
|
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
signature.initVerify(publicKey);
|
||||||
|
signature.update(signatureFileBytes);
|
||||||
|
if (!signature.verify(signatureBytes)) {
|
||||||
|
throw new SignatureException("Signature did not verify");
|
||||||
|
}
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException(
|
||||||
|
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
|
||||||
|
+ " public key from certificate",
|
||||||
|
e);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SignatureException(
|
||||||
|
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
|
||||||
|
+ " public key from certificate",
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the signature into the JAR signature block which is created according to CMS PKCS #7
|
||||||
|
// RFC 5652.
|
||||||
|
// The high-level simplified structure is as follows:
|
||||||
|
// ContentInfo
|
||||||
|
// digestAlgorithm
|
||||||
|
// SignedData
|
||||||
|
// bag of certificates
|
||||||
|
// SignerInfo
|
||||||
|
// signing cert issuer and serial number (for locating the cert in the above bag)
|
||||||
|
// digestAlgorithm
|
||||||
|
// signatureAlgorithm
|
||||||
|
// signature
|
||||||
|
try {
|
||||||
|
SignerInfo signerInfo = new SignerInfo();
|
||||||
|
signerInfo.version = 1;
|
||||||
|
X500Principal signerCertIssuer = signingCert.getIssuerX500Principal();
|
||||||
|
signerInfo.sid =
|
||||||
|
new SignerIdentifier(
|
||||||
|
new IssuerAndSerialNumber(
|
||||||
|
new Asn1OpaqueObject(signerCertIssuer.getEncoded()),
|
||||||
|
signingCert.getSerialNumber()));
|
||||||
|
AlgorithmIdentifier digestAlgorithmId =
|
||||||
|
getSignerInfoDigestAlgorithmOid(digestAlgorithm);
|
||||||
|
AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
|
||||||
|
signerInfo.digestAlgorithm = digestAlgorithmId;
|
||||||
|
signerInfo.signatureAlgorithm = signatureAlgorithmId;
|
||||||
|
signerInfo.signature = ByteBuffer.wrap(signatureBytes);
|
||||||
|
|
||||||
|
SignedData signedData = new SignedData();
|
||||||
|
signedData.certificates = new ArrayList<>(signerCerts.size());
|
||||||
|
for (X509Certificate cert : signerCerts) {
|
||||||
|
signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded()));
|
||||||
|
}
|
||||||
|
signedData.version = 1;
|
||||||
|
signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId);
|
||||||
|
signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA);
|
||||||
|
signedData.signerInfos = Collections.singletonList(signerInfo);
|
||||||
|
|
||||||
|
ContentInfo contentInfo = new ContentInfo();
|
||||||
|
contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA;
|
||||||
|
contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData));
|
||||||
|
return Asn1DerEncoder.encode(contentInfo);
|
||||||
|
} catch (Asn1EncodingException e) {
|
||||||
|
throw new SignatureException("Failed to encode signature block", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
|
||||||
|
* algorithm.
|
||||||
|
*/
|
||||||
|
private static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
|
||||||
|
DigestAlgorithm digestAlgorithm) {
|
||||||
|
switch (digestAlgorithm) {
|
||||||
|
case SHA1:
|
||||||
|
return new AlgorithmIdentifier(
|
||||||
|
V1SchemeVerifier.Signer.OID_DIGEST_SHA1, ASN1_DER_NULL);
|
||||||
|
case SHA256:
|
||||||
|
return new AlgorithmIdentifier(
|
||||||
|
V1SchemeVerifier.Signer.OID_DIGEST_SHA256, ASN1_DER_NULL);
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Unsupported digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
|
||||||
|
* when signing with the specified key and digest algorithm.
|
||||||
|
*/
|
||||||
|
private static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
|
||||||
|
PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException {
|
||||||
|
String keyAlgorithm = publicKey.getAlgorithm();
|
||||||
|
String jcaDigestPrefixForSigAlg;
|
||||||
|
switch (digestAlgorithm) {
|
||||||
|
case SHA1:
|
||||||
|
jcaDigestPrefixForSigAlg = "SHA1";
|
||||||
|
break;
|
||||||
|
case SHA256:
|
||||||
|
jcaDigestPrefixForSigAlg = "SHA256";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unexpected digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
return Pair.of(
|
||||||
|
jcaDigestPrefixForSigAlg + "withRSA",
|
||||||
|
new AlgorithmIdentifier(V1SchemeVerifier.Signer.OID_SIG_RSA, ASN1_DER_NULL));
|
||||||
|
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
AlgorithmIdentifier sigAlgId;
|
||||||
|
switch (digestAlgorithm) {
|
||||||
|
case SHA1:
|
||||||
|
sigAlgId =
|
||||||
|
new AlgorithmIdentifier(
|
||||||
|
V1SchemeVerifier.Signer.OID_SIG_DSA, ASN1_DER_NULL);
|
||||||
|
break;
|
||||||
|
case SHA256:
|
||||||
|
// DSA signatures with SHA-256 in SignedData are accepted by Android API Level
|
||||||
|
// 21 and higher. However, there are two ways to specify their SignedData
|
||||||
|
// SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
|
||||||
|
// dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
|
||||||
|
// the former.
|
||||||
|
sigAlgId =
|
||||||
|
new AlgorithmIdentifier(
|
||||||
|
V1SchemeVerifier.Signer.OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unexpected digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId);
|
||||||
|
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
return Pair.of(
|
||||||
|
jcaDigestPrefixForSigAlg + "withECDSA",
|
||||||
|
new AlgorithmIdentifier(
|
||||||
|
V1SchemeVerifier.Signer.OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
|
||||||
|
} else {
|
||||||
|
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
|
||||||
|
switch (digestAlgorithm) {
|
||||||
|
case SHA1:
|
||||||
|
return "SHA1-Digest";
|
||||||
|
case SHA256:
|
||||||
|
return "SHA-256-Digest";
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unexpected content digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
|
||||||
|
switch (digestAlgorithm) {
|
||||||
|
case SHA1:
|
||||||
|
return "SHA1-Digest-Manifest";
|
||||||
|
case SHA256:
|
||||||
|
return "SHA-256-Digest-Manifest";
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unexpected content digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,294 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk.v2;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.RunnablesExecutor;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.interfaces.ECKey;
|
||||||
|
import java.security.interfaces.RSAKey;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme v2 signer.
|
||||||
|
*
|
||||||
|
* <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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V2SchemeSigner() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
|
||||||
|
* provided key.
|
||||||
|
*
|
||||||
|
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
||||||
|
* AndroidManifest.xml minSdkVersion attribute).
|
||||||
|
*
|
||||||
|
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using
|
||||||
|
* APK Signature Scheme v2
|
||||||
|
*/
|
||||||
|
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
|
||||||
|
PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported)
|
||||||
|
throws InvalidKeyException {
|
||||||
|
String keyAlgorithm = signingKey.getAlgorithm();
|
||||||
|
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
|
||||||
|
// deterministic signatures which make life easier for OTA updates (fewer files
|
||||||
|
// changed when deterministic signature schemes are used).
|
||||||
|
|
||||||
|
// Pick a digest which is no weaker than the key.
|
||||||
|
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
|
||||||
|
if (modulusLengthBits <= 3072) {
|
||||||
|
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
|
||||||
|
if (apkSigningBlockPaddingSupported) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else {
|
||||||
|
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
|
||||||
|
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||||
|
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
|
||||||
|
}
|
||||||
|
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// DSA is supported only with SHA-256.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256);
|
||||||
|
if (apkSigningBlockPaddingSupported) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Pick a digest which is no weaker than the key.
|
||||||
|
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
|
||||||
|
if (keySizeBits <= 256) {
|
||||||
|
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
|
||||||
|
if (apkSigningBlockPaddingSupported) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else {
|
||||||
|
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
|
||||||
|
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||||
|
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
DataSource beforeCentralDir,
|
||||||
|
DataSource centralDir,
|
||||||
|
DataSource eocd,
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
boolean v3SigningEnabled)
|
||||||
|
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
|
||||||
|
SignatureException {
|
||||||
|
Pair<List<SignerConfig>,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]>> digestInfo =
|
||||||
|
ApkSigningBlockUtils.computeContentDigests(
|
||||||
|
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
|
||||||
|
return generateApkSignatureSchemeV2Block(
|
||||||
|
digestInfo.getFirst(), digestInfo.getSecond(),v3SigningEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> contentDigests,
|
||||||
|
boolean v3SigningEnabled)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed sequence of length-prefixed signer blocks.
|
||||||
|
|
||||||
|
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
|
||||||
|
int signerNumber = 0;
|
||||||
|
for (SignerConfig signerConfig : signerConfigs) {
|
||||||
|
signerNumber++;
|
||||||
|
byte[] signerBlock;
|
||||||
|
try {
|
||||||
|
signerBlock = generateSignerBlock(signerConfig, contentDigests, v3SigningEnabled);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SignatureException("Signer #" + signerNumber + " failed", e);
|
||||||
|
}
|
||||||
|
signerBlocks.add(signerBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair.of(encodeAsSequenceOfLengthPrefixedElements(
|
||||||
|
new byte[][] {
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
|
||||||
|
}), APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateSignerBlock(
|
||||||
|
SignerConfig signerConfig,
|
||||||
|
Map<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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute to check whether a newer APK Signature Scheme signature was stripped
|
||||||
|
protected static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
|
||||||
|
|
||||||
|
private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) {
|
||||||
|
if (v3SigningEnabled) {
|
||||||
|
// FORMAT (little endian):
|
||||||
|
// * length-prefixed bytes: attribute pair
|
||||||
|
// * uint32: ID - STRIPPING_PROTECTION_ATTR_ID in this case
|
||||||
|
// * uint32: value - 3 (v3 signature scheme id) in this case
|
||||||
|
int payloadSize = 4 + 4 + 4;
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.putInt(payloadSize - 4);
|
||||||
|
result.putInt(STRIPPING_PROTECTION_ATTR_ID);
|
||||||
|
result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
|
||||||
|
return result.array();
|
||||||
|
} else {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class V2SignatureSchemeBlock {
|
||||||
|
private static final class Signer {
|
||||||
|
public byte[] signedData;
|
||||||
|
public List<Pair<Integer, byte[]>> signatures;
|
||||||
|
public byte[] publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SignedData {
|
||||||
|
public List<Pair<Integer, byte[]>> digests;
|
||||||
|
public List<byte[]> certificates;
|
||||||
|
public byte[] additionalAttributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,462 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk.v2;
|
||||||
|
|
||||||
|
import com.android.apksig.ApkVerifier.Issue;
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.apk.ApkUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureInfo;
|
||||||
|
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||||
|
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.RunnablesExecutor;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme v2 verifier.
|
||||||
|
*
|
||||||
|
* <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 {
|
||||||
|
|
||||||
|
private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V2SchemeVerifier() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of
|
||||||
|
* verification. The APK must be considered verified only if
|
||||||
|
* {@link ApkSigningBlockUtils.Result#verified} is
|
||||||
|
* {@code true}. If verification fails, the result will contain errors -- see
|
||||||
|
* {@link ApkSigningBlockUtils.Result#getErrors()}.
|
||||||
|
*
|
||||||
|
* <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,
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
private static void parseSigners(
|
||||||
|
ByteBuffer apkSignatureSchemeV2Block,
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||||
|
Map<Integer, String> supportedApkSigSchemeNames,
|
||||||
|
Set<Integer> foundApkSigSchemeIds,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion,
|
||||||
|
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
|
||||||
|
ByteBuffer signers;
|
||||||
|
try {
|
||||||
|
signers = ApkSigningBlockUtils.getLengthPrefixedSlice(apkSignatureSchemeV2Block);
|
||||||
|
} catch (ApkFormatException e) {
|
||||||
|
result.addError(Issue.V2_SIG_MALFORMED_SIGNERS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!signers.hasRemaining()) {
|
||||||
|
result.addError(Issue.V2_SIG_NO_SIGNERS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CertificateFactory certFactory;
|
||||||
|
try {
|
||||||
|
certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||||
|
}
|
||||||
|
int signerCount = 0;
|
||||||
|
while (signers.hasRemaining()) {
|
||||||
|
int signerIndex = signerCount;
|
||||||
|
signerCount++;
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||||
|
signerInfo.index = signerIndex;
|
||||||
|
result.signers.add(signerInfo);
|
||||||
|
try {
|
||||||
|
ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers);
|
||||||
|
parseSigner(
|
||||||
|
signer,
|
||||||
|
certFactory,
|
||||||
|
signerInfo,
|
||||||
|
contentDigestsToVerify,
|
||||||
|
supportedApkSigSchemeNames,
|
||||||
|
foundApkSigSchemeIds,
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion);
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the provided signer block and populates the {@code result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
|
||||||
|
* verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
|
||||||
|
* method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
|
||||||
|
* integrity of the APK.
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the
|
||||||
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
private static void parseSigner(
|
||||||
|
ByteBuffer signerBlock,
|
||||||
|
CertificateFactory certFactory,
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo result,
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||||
|
Map<Integer, String> supportedApkSigSchemeNames,
|
||||||
|
Set<Integer> foundApkSigSchemeIds,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion)
|
||||||
|
throws ApkFormatException, NoSuchAlgorithmException {
|
||||||
|
ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
|
||||||
|
byte[] signedDataBytes = new byte[signedData.remaining()];
|
||||||
|
signedData.get(signedDataBytes);
|
||||||
|
signedData.flip();
|
||||||
|
result.signedData = signedDataBytes;
|
||||||
|
|
||||||
|
ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
|
||||||
|
byte[] publicKeyBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signerBlock);
|
||||||
|
|
||||||
|
// Parse the signatures block and identify supported signatures
|
||||||
|
int signatureCount = 0;
|
||||||
|
List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
|
||||||
|
while (signatures.hasRemaining()) {
|
||||||
|
signatureCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
|
||||||
|
int sigAlgorithmId = signature.getInt();
|
||||||
|
byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
|
||||||
|
result.signatures.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.Signature(
|
||||||
|
sigAlgorithmId, sigBytes));
|
||||||
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||||
|
if (signatureAlgorithm == null) {
|
||||||
|
result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
supportedSignatures.add(
|
||||||
|
new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.signatures.isEmpty()) {
|
||||||
|
result.addError(Issue.V2_SIG_NO_SIGNATURES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signatures over signed-data block using the public key
|
||||||
|
List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
|
||||||
|
try {
|
||||||
|
signaturesToVerify =
|
||||||
|
ApkSigningBlockUtils.getSignaturesToVerify(
|
||||||
|
supportedSignatures, minSdkVersion, maxSdkVersion);
|
||||||
|
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
|
||||||
|
result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
|
||||||
|
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
||||||
|
String jcaSignatureAlgorithm =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||||
|
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||||
|
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
|
||||||
|
PublicKey publicKey;
|
||||||
|
try {
|
||||||
|
publicKey =
|
||||||
|
KeyFactory.getInstance(keyAlgorithm).generatePublic(
|
||||||
|
new X509EncodedKeySpec(publicKeyBytes));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
sig.initVerify(publicKey);
|
||||||
|
if (jcaSignatureAlgorithmParams != null) {
|
||||||
|
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||||
|
}
|
||||||
|
signedData.position(0);
|
||||||
|
sig.update(signedData);
|
||||||
|
byte[] sigBytes = signature.signature;
|
||||||
|
if (!sig.verify(sigBytes)) {
|
||||||
|
result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
|
||||||
|
contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
|
||||||
|
} catch (InvalidKeyException | InvalidAlgorithmParameterException
|
||||||
|
| SignatureException e) {
|
||||||
|
result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least one signature over signedData has verified. We can now parse signed-data.
|
||||||
|
signedData.position(0);
|
||||||
|
ByteBuffer digests = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
|
||||||
|
ByteBuffer certificates = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
|
||||||
|
ByteBuffer additionalAttributes = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
|
||||||
|
|
||||||
|
// Parse the certificates block
|
||||||
|
int certificateIndex = -1;
|
||||||
|
while (certificates.hasRemaining()) {
|
||||||
|
certificateIndex++;
|
||||||
|
byte[] encodedCert = ApkSigningBlockUtils.readLengthPrefixedByteArray(certificates);
|
||||||
|
X509Certificate certificate;
|
||||||
|
try {
|
||||||
|
certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V2_SIG_MALFORMED_CERTIFICATE,
|
||||||
|
certificateIndex,
|
||||||
|
certificateIndex + 1,
|
||||||
|
e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||||
|
// form. Without this, getEncoded may return a different form from what was stored in
|
||||||
|
// the signature. This is because some X509Certificate(Factory) implementations
|
||||||
|
// re-encode certificates.
|
||||||
|
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||||
|
result.certs.add(certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.certs.isEmpty()) {
|
||||||
|
result.addError(Issue.V2_SIG_NO_CERTIFICATES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
X509Certificate mainCertificate = result.certs.get(0);
|
||||||
|
byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
|
||||||
|
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
|
||||||
|
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
|
||||||
|
ApkSigningBlockUtils.toHex(publicKeyBytes));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the digests block
|
||||||
|
int digestCount = 0;
|
||||||
|
while (digests.hasRemaining()) {
|
||||||
|
digestCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer digest = ApkSigningBlockUtils.getLengthPrefixedSlice(digests);
|
||||||
|
int sigAlgorithmId = digest.getInt();
|
||||||
|
byte[] digestBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(digest);
|
||||||
|
result.contentDigests.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
|
||||||
|
sigAlgorithmId, digestBytes));
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<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 V2SchemeSigner.STRIPPING_PROTECTION_ATTR_ID:
|
||||||
|
// stripping protection added when signing with a newer scheme
|
||||||
|
int foundId = ByteBuffer.wrap(value).order(
|
||||||
|
ByteOrder.LITTLE_ENDIAN).getInt();
|
||||||
|
if (supportedApkSigSchemeIds.contains(foundId)) {
|
||||||
|
supportedExpectedApkSigSchemeIds.add(foundId);
|
||||||
|
} else {
|
||||||
|
result.addWarning(
|
||||||
|
Issue.V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID, result.index, foundId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
|
||||||
|
}
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that all known IDs indicated in stripping protection have already verified
|
||||||
|
for (int id : supportedExpectedApkSigSchemeIds) {
|
||||||
|
if (!foundApkSigSchemeIds.contains(id)) {
|
||||||
|
String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
|
||||||
|
result.addError(
|
||||||
|
Issue.V2_SIG_MISSING_APK_SIG_REFERENCED,
|
||||||
|
result.index,
|
||||||
|
apkSigSchemeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk.v3;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
|
||||||
|
|
||||||
|
import com.android.apksig.SigningCertificateLineage;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.RunnablesExecutor;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.interfaces.ECKey;
|
||||||
|
import java.security.interfaces.RSAKey;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme v3 signer.
|
||||||
|
*
|
||||||
|
* <p>APK Signature Scheme v3 builds upon APK Signature Scheme v3, and maintains all of the APK
|
||||||
|
* Signature Scheme v2 goals.
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
|
||||||
|
*
|
||||||
|
* <p> The main contribution of APK Signature Scheme v3 is the introduction of the
|
||||||
|
* {@link SigningCertificateLineage}, which enables an APK to change its signing
|
||||||
|
* certificate as long as it can prove the new siging certificate was signed by the old.
|
||||||
|
*/
|
||||||
|
public abstract class V3SchemeSigner {
|
||||||
|
|
||||||
|
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V3SchemeSigner() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the
|
||||||
|
* provided key.
|
||||||
|
*
|
||||||
|
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
||||||
|
* AndroidManifest.xml minSdkVersion attribute).
|
||||||
|
*
|
||||||
|
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using
|
||||||
|
* APK Signature Scheme v3
|
||||||
|
*/
|
||||||
|
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(
|
||||||
|
PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported)
|
||||||
|
throws InvalidKeyException {
|
||||||
|
String keyAlgorithm = signingKey.getAlgorithm();
|
||||||
|
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
|
||||||
|
// deterministic signatures which make life easier for OTA updates (fewer files
|
||||||
|
// changed when deterministic signature schemes are used).
|
||||||
|
|
||||||
|
// Pick a digest which is no weaker than the key.
|
||||||
|
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
|
||||||
|
if (modulusLengthBits <= 3072) {
|
||||||
|
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
|
||||||
|
if (apkSigningBlockPaddingSupported) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else {
|
||||||
|
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
|
||||||
|
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||||
|
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
|
||||||
|
}
|
||||||
|
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// DSA is supported only with SHA-256.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256);
|
||||||
|
if (apkSigningBlockPaddingSupported) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Pick a digest which is no weaker than the key.
|
||||||
|
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
|
||||||
|
if (keySizeBits <= 256) {
|
||||||
|
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
|
||||||
|
if (apkSigningBlockPaddingSupported) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else {
|
||||||
|
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
|
||||||
|
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||||
|
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
DataSource beforeCentralDir,
|
||||||
|
DataSource centralDir,
|
||||||
|
DataSource eocd,
|
||||||
|
List<SignerConfig> signerConfigs)
|
||||||
|
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
|
||||||
|
SignatureException {
|
||||||
|
Pair<List<SignerConfig>,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]>> digestInfo =
|
||||||
|
ApkSigningBlockUtils.computeContentDigests(
|
||||||
|
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
|
||||||
|
return generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> contentDigests)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed sequence of length-prefixed signer blocks.
|
||||||
|
|
||||||
|
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
|
||||||
|
int signerNumber = 0;
|
||||||
|
for (SignerConfig signerConfig : signerConfigs) {
|
||||||
|
signerNumber++;
|
||||||
|
byte[] signerBlock;
|
||||||
|
try {
|
||||||
|
signerBlock = generateSignerBlock(signerConfig, contentDigests);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SignatureException("Signer #" + signerNumber + " failed", e);
|
||||||
|
}
|
||||||
|
signerBlocks.add(signerBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair.of(encodeAsSequenceOfLengthPrefixedElements(
|
||||||
|
new byte[][] {
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
|
||||||
|
}), APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateSignerBlock(
|
||||||
|
SignerConfig signerConfig,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> contentDigests)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
|
if (signerConfig.certificates.isEmpty()) {
|
||||||
|
throw new SignatureException("No certificates configured for signer");
|
||||||
|
}
|
||||||
|
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||||
|
|
||||||
|
byte[] encodedPublicKey = encodePublicKey(publicKey);
|
||||||
|
|
||||||
|
V3SignatureSchemeBlock.SignedData signedData = new V3SignatureSchemeBlock.SignedData();
|
||||||
|
try {
|
||||||
|
signedData.certificates = encodeCertificates(signerConfig.certificates);
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new SignatureException("Failed to encode certificates", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Pair<Integer, byte[]>> digests =
|
||||||
|
new ArrayList<>(signerConfig.signatureAlgorithms.size());
|
||||||
|
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||||
|
ContentDigestAlgorithm contentDigestAlgorithm =
|
||||||
|
signatureAlgorithm.getContentDigestAlgorithm();
|
||||||
|
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
|
||||||
|
if (contentDigest == null) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
contentDigestAlgorithm + " content digest for " + signatureAlgorithm
|
||||||
|
+ " not computed");
|
||||||
|
}
|
||||||
|
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
|
||||||
|
}
|
||||||
|
signedData.digests = digests;
|
||||||
|
signedData.minSdkVersion = signerConfig.minSdkVersion;
|
||||||
|
signedData.maxSdkVersion = signerConfig.maxSdkVersion;
|
||||||
|
signedData.additionalAttributes = generateAdditionalAttributes(signerConfig);
|
||||||
|
|
||||||
|
V3SignatureSchemeBlock.Signer signer = new V3SignatureSchemeBlock.Signer();
|
||||||
|
|
||||||
|
signer.signedData = encodeSignedData(signedData);
|
||||||
|
|
||||||
|
signer.minSdkVersion = signerConfig.minSdkVersion;
|
||||||
|
signer.maxSdkVersion = signerConfig.maxSdkVersion;
|
||||||
|
signer.publicKey = encodedPublicKey;
|
||||||
|
signer.signatures =
|
||||||
|
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
|
||||||
|
|
||||||
|
|
||||||
|
return encodeSigner(signer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
|
||||||
|
byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData);
|
||||||
|
byte[] signatures =
|
||||||
|
encodeAsLengthPrefixedElement(
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
signer.signatures));
|
||||||
|
byte[] publicKey = encodeAsLengthPrefixedElement(signer.publicKey);
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed signed data
|
||||||
|
// * uint32: minSdkVersion
|
||||||
|
// * uint32: maxSdkVersion
|
||||||
|
// * length-prefixed sequence of length-prefixed signatures:
|
||||||
|
// * uint32: signature algorithm ID
|
||||||
|
// * length-prefixed bytes: signature of signed data
|
||||||
|
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
|
||||||
|
int payloadSize =
|
||||||
|
signedData.length
|
||||||
|
+ 4
|
||||||
|
+ 4
|
||||||
|
+ signatures.length
|
||||||
|
+ publicKey.length;
|
||||||
|
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.put(signedData);
|
||||||
|
result.putInt(signer.minSdkVersion);
|
||||||
|
result.putInt(signer.maxSdkVersion);
|
||||||
|
result.put(signatures);
|
||||||
|
result.put(publicKey);
|
||||||
|
|
||||||
|
return result.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
|
||||||
|
byte[] digests =
|
||||||
|
encodeAsLengthPrefixedElement(
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
signedData.digests));
|
||||||
|
byte[] certs =
|
||||||
|
encodeAsLengthPrefixedElement(
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates));
|
||||||
|
byte[] attributes = encodeAsLengthPrefixedElement(signedData.additionalAttributes);
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed sequence of length-prefixed digests:
|
||||||
|
// * uint32: signature algorithm ID
|
||||||
|
// * length-prefixed bytes: digest of contents
|
||||||
|
// * length-prefixed sequence of certificates:
|
||||||
|
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
||||||
|
// * uint-32: minSdkVersion
|
||||||
|
// * uint-32: maxSdkVersion
|
||||||
|
// * length-prefixed sequence of length-prefixed additional attributes:
|
||||||
|
// * uint32: ID
|
||||||
|
// * (length - 4) bytes: value
|
||||||
|
// * uint32: Proof-of-rotation ID: 0x3ba06f8c
|
||||||
|
// * length-prefixed roof-of-rotation structure
|
||||||
|
int payloadSize =
|
||||||
|
digests.length
|
||||||
|
+ certs.length
|
||||||
|
+ 4
|
||||||
|
+ 4
|
||||||
|
+ attributes.length;
|
||||||
|
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.put(digests);
|
||||||
|
result.put(certs);
|
||||||
|
result.putInt(signedData.minSdkVersion);
|
||||||
|
result.putInt(signedData.maxSdkVersion);
|
||||||
|
result.put(attributes);
|
||||||
|
|
||||||
|
return result.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
|
||||||
|
|
||||||
|
private static byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
|
||||||
|
if (signerConfig.mSigningCertificateLineage == null) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
return signerConfig.mSigningCertificateLineage.generateV3SignerAttribute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class V3SignatureSchemeBlock {
|
||||||
|
private static final class Signer {
|
||||||
|
public byte[] signedData;
|
||||||
|
public int minSdkVersion;
|
||||||
|
public int maxSdkVersion;
|
||||||
|
public List<Pair<Integer, byte[]>> signatures;
|
||||||
|
public byte[] publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SignedData {
|
||||||
|
public List<Pair<Integer, byte[]>> digests;
|
||||||
|
public List<byte[]> certificates;
|
||||||
|
public int minSdkVersion;
|
||||||
|
public int maxSdkVersion;
|
||||||
|
public byte[] additionalAttributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,518 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk.v3;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
|
||||||
|
|
||||||
|
import com.android.apksig.ApkVerifier.Issue;
|
||||||
|
import com.android.apksig.SigningCertificateLineage;
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.apk.ApkUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignatureNotFoundException;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureInfo;
|
||||||
|
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||||
|
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||||
|
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.RunnablesExecutor;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme v3 verifier.
|
||||||
|
*
|
||||||
|
* <p>APK Signature Scheme v3, like v2 is a whole-file signature scheme which aims to protect every
|
||||||
|
* single bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
||||||
|
* uncompressed contents of ZIP entries.
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
|
||||||
|
*/
|
||||||
|
public abstract class V3SchemeVerifier {
|
||||||
|
|
||||||
|
private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V3SchemeVerifier() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of
|
||||||
|
* verification. The APK must be considered verified only if
|
||||||
|
* {@link ApkSigningBlockUtils.Result#verified} is
|
||||||
|
* {@code true}. If verification fails, the result will contain errors -- see
|
||||||
|
* {@link ApkSigningBlockUtils.Result#getErrors()}.
|
||||||
|
*
|
||||||
|
* <p>Verification succeeds iff the APK's APK Signature Scheme v3 signatures are expected to
|
||||||
|
* verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
* If the APK's signature is expected to not verify on any of the specified platform versions,
|
||||||
|
* this method returns a result with one or more errors and whose
|
||||||
|
* {@code Result.verified == false}, or this method throws an exception.
|
||||||
|
*
|
||||||
|
* @throws ApkFormatException if the APK is malformed
|
||||||
|
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||||
|
* required cryptographic algorithm implementation is missing
|
||||||
|
* @throws SignatureNotFoundException if no APK Signature Scheme v3
|
||||||
|
* signatures are found
|
||||||
|
* @throws IOException if an I/O error occurs when reading the APK
|
||||||
|
*/
|
||||||
|
public static ApkSigningBlockUtils.Result verify(
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
DataSource apk,
|
||||||
|
ApkUtils.ZipSections zipSections,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion)
|
||||||
|
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
||||||
|
ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
|
||||||
|
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
|
||||||
|
SignatureInfo signatureInfo =
|
||||||
|
ApkSigningBlockUtils.findSignature(apk, zipSections,
|
||||||
|
APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
|
||||||
|
|
||||||
|
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
|
||||||
|
DataSource centralDir =
|
||||||
|
apk.slice(
|
||||||
|
signatureInfo.centralDirOffset,
|
||||||
|
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
|
||||||
|
ByteBuffer eocd = signatureInfo.eocd;
|
||||||
|
|
||||||
|
// v3 didn't exist prior to P, so make sure that we're only judging v3 on its supported
|
||||||
|
// platforms
|
||||||
|
if (minSdkVersion < AndroidSdkVersion.P) {
|
||||||
|
minSdkVersion = AndroidSdkVersion.P;
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(executor,
|
||||||
|
beforeApkSigningBlock,
|
||||||
|
signatureInfo.signatureBlock,
|
||||||
|
centralDir,
|
||||||
|
eocd,
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion,
|
||||||
|
result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's v3 signatures and outputs the results into the provided
|
||||||
|
* {@code result}. APK is considered verified only if there are no errors reported in the
|
||||||
|
* {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
|
||||||
|
* int)} for more information about the contract of this method.
|
||||||
|
*
|
||||||
|
* @param result result populated by this method with interesting information about the APK,
|
||||||
|
* such as information about signers, and verification errors and warnings.
|
||||||
|
*/
|
||||||
|
private static void verify(
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
DataSource beforeApkSigningBlock,
|
||||||
|
ByteBuffer apkSignatureSchemeV3Block,
|
||||||
|
DataSource centralDir,
|
||||||
|
ByteBuffer eocd,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion,
|
||||||
|
ApkSigningBlockUtils.Result result)
|
||||||
|
throws IOException, NoSuchAlgorithmException {
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
|
||||||
|
parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, result);
|
||||||
|
|
||||||
|
if (result.containsErrors()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ApkSigningBlockUtils.verifyIntegrity(
|
||||||
|
executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
|
||||||
|
|
||||||
|
// make sure that the v3 signers cover the entire targeted sdk version ranges and that the
|
||||||
|
// longest SigningCertificateHistory, if present, corresponds to the newest platform
|
||||||
|
// versions
|
||||||
|
SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo signer : result.signers) {
|
||||||
|
sortedSigners.put(signer.minSdkVersion, signer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// first make sure there is neither overlap nor holes
|
||||||
|
int firstMin = 0;
|
||||||
|
int lastMax = 0;
|
||||||
|
int lastLineageSize = 0;
|
||||||
|
|
||||||
|
// while we're iterating through the signers, build up the list of lineages
|
||||||
|
List<SigningCertificateLineage> lineages = new ArrayList<>(result.signers.size());
|
||||||
|
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) {
|
||||||
|
int currentMin = signer.minSdkVersion;
|
||||||
|
int currentMax = signer.maxSdkVersion;
|
||||||
|
if (firstMin == 0) {
|
||||||
|
// first round sets up our basis
|
||||||
|
firstMin = currentMin;
|
||||||
|
} else {
|
||||||
|
if (currentMin != lastMax + 1) {
|
||||||
|
result.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastMax = currentMax;
|
||||||
|
|
||||||
|
// also, while we're here, make sure that the lineage sizes only increase
|
||||||
|
if (signer.signingCertificateLineage != null) {
|
||||||
|
int currLineageSize = signer.signingCertificateLineage.size();
|
||||||
|
if (currLineageSize < lastLineageSize) {
|
||||||
|
result.addError(Issue.V3_INCONSISTENT_LINEAGES);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lastLineageSize = currLineageSize;
|
||||||
|
lineages.add(signer.signingCertificateLineage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we support our desired sdk ranges
|
||||||
|
if (firstMin > minSdkVersion || lastMax < maxSdkVersion) {
|
||||||
|
result.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
result.signingCertificateLineage =
|
||||||
|
SigningCertificateLineage.consolidateLineages(lineages);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
result.addError(Issue.V3_INCONSISTENT_LINEAGES);
|
||||||
|
}
|
||||||
|
if (!result.containsErrors()) {
|
||||||
|
result.verified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses each signer in the provided APK Signature Scheme v3 block and populates corresponding
|
||||||
|
* {@code signerInfos} of the provided {@code result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
|
||||||
|
* However, this does not verify the integrity of the rest of the APK but rather simply reports
|
||||||
|
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the
|
||||||
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
private static void parseSigners(
|
||||||
|
ByteBuffer apkSignatureSchemeV3Block,
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||||
|
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
|
||||||
|
ByteBuffer signers;
|
||||||
|
try {
|
||||||
|
signers = getLengthPrefixedSlice(apkSignatureSchemeV3Block);
|
||||||
|
} catch (ApkFormatException e) {
|
||||||
|
result.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!signers.hasRemaining()) {
|
||||||
|
result.addError(Issue.V3_SIG_NO_SIGNERS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CertificateFactory certFactory;
|
||||||
|
try {
|
||||||
|
certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||||
|
}
|
||||||
|
int signerCount = 0;
|
||||||
|
while (signers.hasRemaining()) {
|
||||||
|
int signerIndex = signerCount;
|
||||||
|
signerCount++;
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||||
|
signerInfo.index = signerIndex;
|
||||||
|
result.signers.add(signerInfo);
|
||||||
|
try {
|
||||||
|
ByteBuffer signer = getLengthPrefixedSlice(signers);
|
||||||
|
parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify);
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the provided signer block and populates the {@code result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over {@code signed-data} contained in this block, as well as
|
||||||
|
* the data contained therein, but does not verify the integrity of the rest of the APK. To
|
||||||
|
* facilitate APK integrity verification, this method adds the {@code contentDigestsToVerify}.
|
||||||
|
* These digests can then be used to verify the integrity of the APK.
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the
|
||||||
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
private static void parseSigner(
|
||||||
|
ByteBuffer signerBlock,
|
||||||
|
CertificateFactory certFactory,
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo result,
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify)
|
||||||
|
throws ApkFormatException, NoSuchAlgorithmException {
|
||||||
|
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
|
||||||
|
byte[] signedDataBytes = new byte[signedData.remaining()];
|
||||||
|
signedData.get(signedDataBytes);
|
||||||
|
signedData.flip();
|
||||||
|
result.signedData = signedDataBytes;
|
||||||
|
|
||||||
|
int parsedMinSdkVersion = signerBlock.getInt();
|
||||||
|
int parsedMaxSdkVersion = signerBlock.getInt();
|
||||||
|
result.minSdkVersion = parsedMinSdkVersion;
|
||||||
|
result.maxSdkVersion = parsedMaxSdkVersion;
|
||||||
|
if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion);
|
||||||
|
}
|
||||||
|
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
|
||||||
|
byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
|
||||||
|
|
||||||
|
// Parse the signatures block and identify supported signatures
|
||||||
|
int signatureCount = 0;
|
||||||
|
List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
|
||||||
|
while (signatures.hasRemaining()) {
|
||||||
|
signatureCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer signature = getLengthPrefixedSlice(signatures);
|
||||||
|
int sigAlgorithmId = signature.getInt();
|
||||||
|
byte[] sigBytes = readLengthPrefixedByteArray(signature);
|
||||||
|
result.signatures.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.Signature(
|
||||||
|
sigAlgorithmId, sigBytes));
|
||||||
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||||
|
if (signatureAlgorithm == null) {
|
||||||
|
result.addWarning(Issue.V3_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// TODO consider dropping deprecated signatures for v3 or modifying
|
||||||
|
// getSignaturesToVerify (called below)
|
||||||
|
supportedSignatures.add(
|
||||||
|
new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(Issue.V3_SIG_MALFORMED_SIGNATURE, signatureCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.signatures.isEmpty()) {
|
||||||
|
result.addError(Issue.V3_SIG_NO_SIGNATURES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signatures over signed-data block using the public key
|
||||||
|
List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
|
||||||
|
try {
|
||||||
|
signaturesToVerify =
|
||||||
|
ApkSigningBlockUtils.getSignaturesToVerify(
|
||||||
|
supportedSignatures, result.minSdkVersion, result.maxSdkVersion);
|
||||||
|
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
|
||||||
|
result.addError(Issue.V3_SIG_NO_SUPPORTED_SIGNATURES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
|
||||||
|
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
||||||
|
String jcaSignatureAlgorithm =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||||
|
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||||
|
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
|
||||||
|
PublicKey publicKey;
|
||||||
|
try {
|
||||||
|
publicKey =
|
||||||
|
KeyFactory.getInstance(keyAlgorithm).generatePublic(
|
||||||
|
new X509EncodedKeySpec(publicKeyBytes));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addError(Issue.V3_SIG_MALFORMED_PUBLIC_KEY, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
sig.initVerify(publicKey);
|
||||||
|
if (jcaSignatureAlgorithmParams != null) {
|
||||||
|
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||||
|
}
|
||||||
|
signedData.position(0);
|
||||||
|
sig.update(signedData);
|
||||||
|
byte[] sigBytes = signature.signature;
|
||||||
|
if (!sig.verify(sigBytes)) {
|
||||||
|
result.addError(Issue.V3_SIG_DID_NOT_VERIFY, signatureAlgorithm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
|
||||||
|
contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
|
||||||
|
} catch (InvalidKeyException | InvalidAlgorithmParameterException
|
||||||
|
| SignatureException e) {
|
||||||
|
result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least one signature over signedData has verified. We can now parse signed-data.
|
||||||
|
signedData.position(0);
|
||||||
|
ByteBuffer digests = getLengthPrefixedSlice(signedData);
|
||||||
|
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
|
||||||
|
|
||||||
|
int signedMinSdkVersion = signedData.getInt();
|
||||||
|
if (signedMinSdkVersion != parsedMinSdkVersion) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
|
||||||
|
parsedMinSdkVersion,
|
||||||
|
signedMinSdkVersion);
|
||||||
|
}
|
||||||
|
int signedMaxSdkVersion = signedData.getInt();
|
||||||
|
if (signedMaxSdkVersion != parsedMaxSdkVersion) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
|
||||||
|
parsedMaxSdkVersion,
|
||||||
|
signedMaxSdkVersion);
|
||||||
|
}
|
||||||
|
ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
|
||||||
|
|
||||||
|
// Parse the certificates block
|
||||||
|
int certificateIndex = -1;
|
||||||
|
while (certificates.hasRemaining()) {
|
||||||
|
certificateIndex++;
|
||||||
|
byte[] encodedCert = readLengthPrefixedByteArray(certificates);
|
||||||
|
X509Certificate certificate;
|
||||||
|
try {
|
||||||
|
certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_SIG_MALFORMED_CERTIFICATE,
|
||||||
|
certificateIndex,
|
||||||
|
certificateIndex + 1,
|
||||||
|
e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||||
|
// form. Without this, getEncoded may return a different form from what was stored in
|
||||||
|
// the signature. This is because some X509Certificate(Factory) implementations
|
||||||
|
// re-encode certificates.
|
||||||
|
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||||
|
result.certs.add(certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.certs.isEmpty()) {
|
||||||
|
result.addError(Issue.V3_SIG_NO_CERTIFICATES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
X509Certificate mainCertificate = result.certs.get(0);
|
||||||
|
byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
|
||||||
|
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
|
||||||
|
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
|
||||||
|
ApkSigningBlockUtils.toHex(publicKeyBytes));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the digests block
|
||||||
|
int digestCount = 0;
|
||||||
|
while (digests.hasRemaining()) {
|
||||||
|
digestCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer digest = getLengthPrefixedSlice(digests);
|
||||||
|
int sigAlgorithmId = digest.getInt();
|
||||||
|
byte[] digestBytes = readLengthPrefixedByteArray(digest);
|
||||||
|
result.contentDigests.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
|
||||||
|
sigAlgorithmId, digestBytes));
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(Issue.V3_SIG_MALFORMED_DIGEST, digestCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
|
||||||
|
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
|
||||||
|
}
|
||||||
|
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
|
||||||
|
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
|
||||||
|
sigAlgsFromSignaturesRecord,
|
||||||
|
sigAlgsFromDigestsRecord);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the additional attributes block.
|
||||||
|
int additionalAttributeCount = 0;
|
||||||
|
while (additionalAttributes.hasRemaining()) {
|
||||||
|
additionalAttributeCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer attribute =
|
||||||
|
getLengthPrefixedSlice(additionalAttributes);
|
||||||
|
int id = attribute.getInt();
|
||||||
|
byte[] value = ByteBufferUtils.toByteArray(attribute);
|
||||||
|
result.additionalAttributes.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
|
||||||
|
if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) {
|
||||||
|
try {
|
||||||
|
// SigningCertificateLineage is verified when built
|
||||||
|
result.signingCertificateLineage =
|
||||||
|
SigningCertificateLineage.readFromV3AttributeValue(value);
|
||||||
|
// make sure that the last cert in the chain matches this signer cert
|
||||||
|
SigningCertificateLineage subLineage =
|
||||||
|
result.signingCertificateLineage.getSubLineage(result.certs.get(0));
|
||||||
|
if (result.signingCertificateLineage.size() != subLineage.size()) {
|
||||||
|
result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
|
||||||
|
}
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
result.addError(Issue.V3_SIG_POR_DID_NOT_VERIFY);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
|
||||||
|
}
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,306 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.apk.v3;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
|
||||||
|
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signer Lineage.
|
||||||
|
*
|
||||||
|
* <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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the signing cert for this node. This is part of the data signed by the parent node.
|
||||||
|
*/
|
||||||
|
public final X509Certificate signingCert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the algorithm used by the this node's parent to bless this data. Its ID value is part of
|
||||||
|
* the data signed by the parent node. {@code null} for first node.
|
||||||
|
*/
|
||||||
|
public final SignatureAlgorithm parentSigAlgorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the algorithm used by the this nodeto bless the next node's data. Its ID value is part
|
||||||
|
* of the signed data of the next node. {@code null} for the last node.
|
||||||
|
*/
|
||||||
|
public SignatureAlgorithm sigAlgorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* signature over the signed data (above). The signature is from this node's parent
|
||||||
|
* signing certificate, which should correspond to the signing certificate used to sign an
|
||||||
|
* APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
|
||||||
|
*/
|
||||||
|
public final byte[] signature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the flags detailing how the platform should treat this signing cert
|
||||||
|
*/
|
||||||
|
public int flags;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,674 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerDataValue;
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerDataValueFormatException;
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerDataValueReader;
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerEncoding;
|
||||||
|
import com.android.apksig.internal.asn1.ber.ByteBufferBerDataValueReader;
|
||||||
|
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser of ASN.1 BER-encoded structures.
|
||||||
|
*
|
||||||
|
* <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);
|
||||||
|
try {
|
||||||
|
return value.intValue();
|
||||||
|
} catch (ArithmeticException e) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException {
|
||||||
|
BigInteger value = integerToBigInteger(encoded);
|
||||||
|
try {
|
||||||
|
return value.longValue();
|
||||||
|
} catch (ArithmeticException e) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
Field[] declaredFields = containerClass.getDeclaredFields();
|
||||||
|
List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
|
||||||
|
for (Field field : declaredFields) {
|
||||||
|
Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
|
||||||
|
if (annotation == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Modifier.isStatic(field.getModifiers())) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
Asn1Field.class.getName() + " used on a static field: "
|
||||||
|
+ containerClass.getName() + "." + field.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
AnnotatedField annotatedField;
|
||||||
|
try {
|
||||||
|
annotatedField = new AnnotatedField(field, annotation);
|
||||||
|
} catch (Asn1DecodingException e) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Invalid ASN.1 annotation on "
|
||||||
|
+ containerClass.getName() + "." + field.getName(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
result.add(annotatedField);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BerToJavaConverter {
|
||||||
|
private BerToJavaConverter() {}
|
||||||
|
|
||||||
|
public static void setFieldValue(
|
||||||
|
Object obj, Field field, Asn1Type type, BerDataValue dataValue)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case SET_OF:
|
||||||
|
case SEQUENCE_OF:
|
||||||
|
if (Asn1OpaqueObject.class.equals(field.getType())) {
|
||||||
|
field.set(obj, convert(type, dataValue, field.getType()));
|
||||||
|
} else {
|
||||||
|
field.set(obj, parseSetOf(dataValue, getElementType(field)));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
field.set(obj, convert(type, dataValue, field.getType()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Failed to set value of " + obj.getClass().getName()
|
||||||
|
+ "." + field.getName(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> T convert(
|
||||||
|
Asn1Type sourceType,
|
||||||
|
BerDataValue dataValue,
|
||||||
|
Class<T> targetType) throws Asn1DecodingException {
|
||||||
|
if (ByteBuffer.class.equals(targetType)) {
|
||||||
|
return (T) dataValue.getEncodedContents();
|
||||||
|
} else if (byte[].class.equals(targetType)) {
|
||||||
|
ByteBuffer resultBuf = dataValue.getEncodedContents();
|
||||||
|
if (!resultBuf.hasRemaining()) {
|
||||||
|
return (T) EMPTY_BYTE_ARRAY;
|
||||||
|
}
|
||||||
|
byte[] result = new byte[resultBuf.remaining()];
|
||||||
|
resultBuf.get(result);
|
||||||
|
return (T) result;
|
||||||
|
} else if (Asn1OpaqueObject.class.equals(targetType)) {
|
||||||
|
return (T) new Asn1OpaqueObject(dataValue.getEncoded());
|
||||||
|
}
|
||||||
|
ByteBuffer encodedContents = dataValue.getEncodedContents();
|
||||||
|
switch (sourceType) {
|
||||||
|
case INTEGER:
|
||||||
|
if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) {
|
||||||
|
return (T) Integer.valueOf(integerToInt(encodedContents));
|
||||||
|
} else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) {
|
||||||
|
return (T) Long.valueOf(integerToLong(encodedContents));
|
||||||
|
} else if (BigInteger.class.equals(targetType)) {
|
||||||
|
return (T) integerToBigInteger(encodedContents);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OBJECT_IDENTIFIER:
|
||||||
|
if (String.class.equals(targetType)) {
|
||||||
|
return (T) oidToString(encodedContents);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UTC_TIME:
|
||||||
|
case GENERALIZED_TIME:
|
||||||
|
if (String.class.equals(targetType)) {
|
||||||
|
return (T) new String(ByteBufferUtils.toByteArray(encodedContents));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BOOLEAN:
|
||||||
|
// A boolean should be encoded in a single byte with a value of 0 for false and
|
||||||
|
// any non-zero value for true.
|
||||||
|
if (boolean.class.equals(targetType)) {
|
||||||
|
if (encodedContents.remaining() != 1) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Incorrect encoded size of boolean value: "
|
||||||
|
+ encodedContents.remaining());
|
||||||
|
}
|
||||||
|
boolean result;
|
||||||
|
if (encodedContents.get() == 0) {
|
||||||
|
result = false;
|
||||||
|
} else {
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
return (T) new Boolean(result);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SEQUENCE:
|
||||||
|
{
|
||||||
|
Asn1Class containerAnnotation =
|
||||||
|
targetType.getDeclaredAnnotation(Asn1Class.class);
|
||||||
|
if ((containerAnnotation != null)
|
||||||
|
&& (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
|
||||||
|
return parseSequence(dataValue, targetType);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CHOICE:
|
||||||
|
{
|
||||||
|
Asn1Class containerAnnotation =
|
||||||
|
targetType.getDeclaredAnnotation(Asn1Class.class);
|
||||||
|
if ((containerAnnotation != null)
|
||||||
|
&& (containerAnnotation.type() == Asn1Type.CHOICE)) {
|
||||||
|
return parseChoice(dataValue, targetType);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Target({ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Asn1Class {
|
||||||
|
public Asn1Type type();
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that input could not be decoded into intended ASN.1 structure.
|
||||||
|
*/
|
||||||
|
public class Asn1DecodingException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public Asn1DecodingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Asn1DecodingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,593 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerEncoding;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encoder of ASN.1 structures into DER-encoded form.
|
||||||
|
*
|
||||||
|
* <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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an ASN.1 structure could not be encoded.
|
||||||
|
*/
|
||||||
|
public class Asn1EncodingException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public Asn1EncodingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Asn1EncodingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Target({ElementType.FIELD})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Asn1Field {
|
||||||
|
/** Index used to order fields in a container. Required for fields of SEQUENCE containers. */
|
||||||
|
public int index() default 0;
|
||||||
|
|
||||||
|
public Asn1TagClass cls() default Asn1TagClass.AUTOMATIC;
|
||||||
|
|
||||||
|
public Asn1Type type();
|
||||||
|
|
||||||
|
/** Tagging mode. Default: NORMAL. */
|
||||||
|
public Asn1Tagging tagging() default Asn1Tagging.NORMAL;
|
||||||
|
|
||||||
|
/** Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.*/
|
||||||
|
public int tagNumber() default -1;
|
||||||
|
|
||||||
|
/** {@code true} if this field is optional. Ignored for fields of CHOICE containers. */
|
||||||
|
public boolean optional() default false;
|
||||||
|
|
||||||
|
/** Type of elements. Used only for SET_OF or SEQUENCE_OF. */
|
||||||
|
public Asn1Type elementType() default Asn1Type.ANY;
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opaque holder of encoded ASN.1 stuff.
|
||||||
|
*/
|
||||||
|
public class Asn1OpaqueObject {
|
||||||
|
private final ByteBuffer mEncoded;
|
||||||
|
|
||||||
|
public Asn1OpaqueObject(ByteBuffer encoded) {
|
||||||
|
mEncoded = encoded.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Asn1OpaqueObject(byte[] encoded) {
|
||||||
|
mEncoded = ByteBuffer.wrap(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getEncoded() {
|
||||||
|
return mEncoded.slice();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1;
|
||||||
|
|
||||||
|
public enum Asn1TagClass {
|
||||||
|
UNIVERSAL,
|
||||||
|
APPLICATION,
|
||||||
|
CONTEXT_SPECIFIC,
|
||||||
|
PRIVATE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class
|
||||||
|
* automatically.
|
||||||
|
*/
|
||||||
|
AUTOMATIC,
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1;
|
||||||
|
|
||||||
|
public enum Asn1Tagging {
|
||||||
|
NORMAL,
|
||||||
|
EXPLICIT,
|
||||||
|
IMPLICIT,
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1;
|
||||||
|
|
||||||
|
public enum Asn1Type {
|
||||||
|
ANY,
|
||||||
|
CHOICE,
|
||||||
|
INTEGER,
|
||||||
|
OBJECT_IDENTIFIER,
|
||||||
|
OCTET_STRING,
|
||||||
|
SEQUENCE,
|
||||||
|
SEQUENCE_OF,
|
||||||
|
SET_OF,
|
||||||
|
BIT_STRING,
|
||||||
|
UTC_TIME,
|
||||||
|
GENERALIZED_TIME,
|
||||||
|
BOOLEAN,
|
||||||
|
// This type can be used to annotate classes that encapsulate ASN.1 structures that are not
|
||||||
|
// classified as a SEQUENCE or SET.
|
||||||
|
UNENCODED_CONTAINER
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}.
|
||||||
|
*/
|
||||||
|
public class BerDataValue {
|
||||||
|
private final ByteBuffer mEncoded;
|
||||||
|
private final ByteBuffer mEncodedContents;
|
||||||
|
private final int mTagClass;
|
||||||
|
private final boolean mConstructed;
|
||||||
|
private final int mTagNumber;
|
||||||
|
|
||||||
|
BerDataValue(
|
||||||
|
ByteBuffer encoded,
|
||||||
|
ByteBuffer encodedContents,
|
||||||
|
int tagClass,
|
||||||
|
boolean constructed,
|
||||||
|
int tagNumber) {
|
||||||
|
mEncoded = encoded;
|
||||||
|
mEncodedContents = encodedContents;
|
||||||
|
mTagClass = tagClass;
|
||||||
|
mConstructed = constructed;
|
||||||
|
mTagNumber = tagNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS}
|
||||||
|
* constants.
|
||||||
|
*/
|
||||||
|
public int getTagClass() {
|
||||||
|
return mTagClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the content octets of this data value are the complete BER encoding
|
||||||
|
* of one or more data values, {@code false} if the content octets of this data value directly
|
||||||
|
* represent the value.
|
||||||
|
*/
|
||||||
|
public boolean isConstructed() {
|
||||||
|
return mConstructed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER}
|
||||||
|
* constants.
|
||||||
|
*/
|
||||||
|
public int getTagNumber() {
|
||||||
|
return mTagNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the encoded form of this data value.
|
||||||
|
*/
|
||||||
|
public ByteBuffer getEncoded() {
|
||||||
|
return mEncoded.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the encoded contents of this data value.
|
||||||
|
*/
|
||||||
|
public ByteBuffer getEncodedContents() {
|
||||||
|
return mEncodedContents.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new reader of the contents of this data value.
|
||||||
|
*/
|
||||||
|
public BerDataValueReader contentsReader() {
|
||||||
|
return new ByteBufferBerDataValueReader(getEncodedContents());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new reader which returns just this data value. This may be useful for re-reading
|
||||||
|
* this value in different contexts.
|
||||||
|
*/
|
||||||
|
public BerDataValueReader dataValueReader() {
|
||||||
|
return new ParsedValueReader(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ParsedValueReader implements BerDataValueReader {
|
||||||
|
private final BerDataValue mValue;
|
||||||
|
private boolean mValueOutput;
|
||||||
|
|
||||||
|
public ParsedValueReader(BerDataValue value) {
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BerDataValue readDataValue() throws BerDataValueFormatException {
|
||||||
|
if (mValueOutput) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
mValueOutput = true;
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an ASN.1 data value being read could not be decoded using
|
||||||
|
* Basic Encoding Rules (BER).
|
||||||
|
*/
|
||||||
|
public class BerDataValueFormatException extends Exception {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public BerDataValueFormatException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BerDataValueFormatException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reader of ASN.1 Basic Encoding Rules (BER) data values.
|
||||||
|
*
|
||||||
|
* <p>BER data value reader returns data values, one by one, from a source. The interpretation of
|
||||||
|
* data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract
|
||||||
|
* the elements of a SEQUENCE value) is left to clients of the reader.
|
||||||
|
*/
|
||||||
|
public interface BerDataValueReader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next data value or {@code null} if end of input has been reached.
|
||||||
|
*
|
||||||
|
* @throws BerDataValueFormatException if the value being read is malformed.
|
||||||
|
*/
|
||||||
|
BerDataValue readDataValue() throws BerDataValueFormatException;
|
||||||
|
}
|
|
@ -0,0 +1,225 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1TagClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}.
|
||||||
|
*/
|
||||||
|
public abstract class BerEncoding {
|
||||||
|
private BerEncoding() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructed vs primitive flag in the first identifier byte.
|
||||||
|
*/
|
||||||
|
public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag class: UNIVERSAL
|
||||||
|
*/
|
||||||
|
public static final int TAG_CLASS_UNIVERSAL = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag class: APPLICATION
|
||||||
|
*/
|
||||||
|
public static final int TAG_CLASS_APPLICATION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag class: CONTEXT SPECIFIC
|
||||||
|
*/
|
||||||
|
public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag class: PRIVATE
|
||||||
|
*/
|
||||||
|
public static final int TAG_CLASS_PRIVATE = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: BOOLEAN
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_BOOLEAN = 0x1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: INTEGER
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_INTEGER = 0x2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: BIT STRING
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_BIT_STRING = 0x3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: OCTET STRING
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_OCTET_STRING = 0x4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: NULL
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_NULL = 0x05;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: OBJECT IDENTIFIER
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: SEQUENCE
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_SEQUENCE = 0x10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: SET
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_SET = 0x11;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: UTC_TIME
|
||||||
|
*/
|
||||||
|
public final static int TAG_NUMBER_UTC_TIME = 0x17;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: GENERALIZED_TIME
|
||||||
|
*/
|
||||||
|
public final static int TAG_NUMBER_GENERALIZED_TIME = 0x18;
|
||||||
|
|
||||||
|
public static int getTagNumber(Asn1Type dataType) {
|
||||||
|
switch (dataType) {
|
||||||
|
case INTEGER:
|
||||||
|
return TAG_NUMBER_INTEGER;
|
||||||
|
case OBJECT_IDENTIFIER:
|
||||||
|
return TAG_NUMBER_OBJECT_IDENTIFIER;
|
||||||
|
case OCTET_STRING:
|
||||||
|
return TAG_NUMBER_OCTET_STRING;
|
||||||
|
case BIT_STRING:
|
||||||
|
return TAG_NUMBER_BIT_STRING;
|
||||||
|
case SET_OF:
|
||||||
|
return TAG_NUMBER_SET;
|
||||||
|
case SEQUENCE:
|
||||||
|
case SEQUENCE_OF:
|
||||||
|
return TAG_NUMBER_SEQUENCE;
|
||||||
|
case UTC_TIME:
|
||||||
|
return TAG_NUMBER_UTC_TIME;
|
||||||
|
case GENERALIZED_TIME:
|
||||||
|
return TAG_NUMBER_GENERALIZED_TIME;
|
||||||
|
case BOOLEAN:
|
||||||
|
return TAG_NUMBER_BOOLEAN;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported data type: " + dataType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getTagClass(Asn1TagClass tagClass) {
|
||||||
|
switch (tagClass) {
|
||||||
|
case APPLICATION:
|
||||||
|
return TAG_CLASS_APPLICATION;
|
||||||
|
case CONTEXT_SPECIFIC:
|
||||||
|
return TAG_CLASS_CONTEXT_SPECIFIC;
|
||||||
|
case PRIVATE:
|
||||||
|
return TAG_CLASS_PRIVATE;
|
||||||
|
case UNIVERSAL:
|
||||||
|
return TAG_CLASS_UNIVERSAL;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported tag class: " + tagClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String tagClassToString(int typeClass) {
|
||||||
|
switch (typeClass) {
|
||||||
|
case TAG_CLASS_APPLICATION:
|
||||||
|
return "APPLICATION";
|
||||||
|
case TAG_CLASS_CONTEXT_SPECIFIC:
|
||||||
|
return "";
|
||||||
|
case TAG_CLASS_PRIVATE:
|
||||||
|
return "PRIVATE";
|
||||||
|
case TAG_CLASS_UNIVERSAL:
|
||||||
|
return "UNIVERSAL";
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported type class: " + typeClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String tagClassAndNumberToString(int tagClass, int tagNumber) {
|
||||||
|
String classString = tagClassToString(tagClass);
|
||||||
|
String numberString = tagNumberToString(tagNumber);
|
||||||
|
return classString.isEmpty() ? numberString : classString + " " + numberString;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static String tagNumberToString(int tagNumber) {
|
||||||
|
switch (tagNumber) {
|
||||||
|
case TAG_NUMBER_INTEGER:
|
||||||
|
return "INTEGER";
|
||||||
|
case TAG_NUMBER_OCTET_STRING:
|
||||||
|
return "OCTET STRING";
|
||||||
|
case TAG_NUMBER_BIT_STRING:
|
||||||
|
return "BIT STRING";
|
||||||
|
case TAG_NUMBER_NULL:
|
||||||
|
return "NULL";
|
||||||
|
case TAG_NUMBER_OBJECT_IDENTIFIER:
|
||||||
|
return "OBJECT IDENTIFIER";
|
||||||
|
case TAG_NUMBER_SEQUENCE:
|
||||||
|
return "SEQUENCE";
|
||||||
|
case TAG_NUMBER_SET:
|
||||||
|
return "SET";
|
||||||
|
case TAG_NUMBER_BOOLEAN:
|
||||||
|
return "BOOLEAN";
|
||||||
|
case TAG_NUMBER_GENERALIZED_TIME:
|
||||||
|
return "GENERALIZED TIME";
|
||||||
|
case TAG_NUMBER_UTC_TIME:
|
||||||
|
return "UTC TIME";
|
||||||
|
default:
|
||||||
|
return "0x" + Integer.toHexString(tagNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the provided first identifier byte indicates that the data value uses
|
||||||
|
* constructed encoding for its contents, or {@code false} if the data value uses primitive
|
||||||
|
* encoding for its contents.
|
||||||
|
*/
|
||||||
|
public static boolean isConstructed(byte firstIdentifierByte) {
|
||||||
|
return (firstIdentifierByte & ID_FLAG_CONSTRUCTED_ENCODING) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS}
|
||||||
|
* constants.
|
||||||
|
*/
|
||||||
|
public static int getTagClass(byte firstIdentifierByte) {
|
||||||
|
return (firstIdentifierByte & 0xff) >> 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte setTagClass(byte firstIdentifierByte, int tagClass) {
|
||||||
|
return (byte) ((firstIdentifierByte & 0x3f) | (tagClass << 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER}
|
||||||
|
* constants.
|
||||||
|
*/
|
||||||
|
public static int getTagNumber(byte firstIdentifierByte) {
|
||||||
|
return firstIdentifierByte & 0x1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte setTagNumber(byte firstIdentifierByte, int tagNumber) {
|
||||||
|
return (byte) ((firstIdentifierByte & ~0x1f) | tagNumber);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,208 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data
|
||||||
|
* values. See {@code X.690} for the encoding.
|
||||||
|
*/
|
||||||
|
public class ByteBufferBerDataValueReader implements BerDataValueReader {
|
||||||
|
private final ByteBuffer mBuf;
|
||||||
|
|
||||||
|
public ByteBufferBerDataValueReader(ByteBuffer buf) {
|
||||||
|
if (buf == null) {
|
||||||
|
throw new NullPointerException("buf == null");
|
||||||
|
}
|
||||||
|
mBuf = buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BerDataValue readDataValue() throws BerDataValueFormatException {
|
||||||
|
int startPosition = mBuf.position();
|
||||||
|
if (!mBuf.hasRemaining()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte firstIdentifierByte = mBuf.get();
|
||||||
|
int tagNumber = readTagNumber(firstIdentifierByte);
|
||||||
|
boolean constructed = BerEncoding.isConstructed(firstIdentifierByte);
|
||||||
|
|
||||||
|
if (!mBuf.hasRemaining()) {
|
||||||
|
throw new BerDataValueFormatException("Missing length");
|
||||||
|
}
|
||||||
|
int firstLengthByte = mBuf.get() & 0xff;
|
||||||
|
int contentsLength;
|
||||||
|
int contentsOffsetInTag;
|
||||||
|
if ((firstLengthByte & 0x80) == 0) {
|
||||||
|
// short form length
|
||||||
|
contentsLength = readShortFormLength(firstLengthByte);
|
||||||
|
contentsOffsetInTag = mBuf.position() - startPosition;
|
||||||
|
skipDefiniteLengthContents(contentsLength);
|
||||||
|
} else if (firstLengthByte != 0x80) {
|
||||||
|
// long form length
|
||||||
|
contentsLength = readLongFormLength(firstLengthByte);
|
||||||
|
contentsOffsetInTag = mBuf.position() - startPosition;
|
||||||
|
skipDefiniteLengthContents(contentsLength);
|
||||||
|
} else {
|
||||||
|
// indefinite length -- value ends with 0x00 0x00
|
||||||
|
contentsOffsetInTag = mBuf.position() - startPosition;
|
||||||
|
contentsLength =
|
||||||
|
constructed
|
||||||
|
? skipConstructedIndefiniteLengthContents()
|
||||||
|
: skipPrimitiveIndefiniteLengthContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the encoded data value ByteBuffer
|
||||||
|
int endPosition = mBuf.position();
|
||||||
|
mBuf.position(startPosition);
|
||||||
|
int bufOriginalLimit = mBuf.limit();
|
||||||
|
mBuf.limit(endPosition);
|
||||||
|
ByteBuffer encoded = mBuf.slice();
|
||||||
|
mBuf.position(mBuf.limit());
|
||||||
|
mBuf.limit(bufOriginalLimit);
|
||||||
|
|
||||||
|
// Create the encoded contents ByteBuffer
|
||||||
|
encoded.position(contentsOffsetInTag);
|
||||||
|
encoded.limit(contentsOffsetInTag + contentsLength);
|
||||||
|
ByteBuffer encodedContents = encoded.slice();
|
||||||
|
encoded.clear();
|
||||||
|
|
||||||
|
return new BerDataValue(
|
||||||
|
encoded,
|
||||||
|
encodedContents,
|
||||||
|
BerEncoding.getTagClass(firstIdentifierByte),
|
||||||
|
constructed,
|
||||||
|
tagNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readTagNumber(byte firstIdentifierByte) throws BerDataValueFormatException {
|
||||||
|
int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte);
|
||||||
|
if (tagNumber == 0x1f) {
|
||||||
|
// high-tag-number form, where the tag number follows this byte in base-128
|
||||||
|
// big-endian form, where each byte has the highest bit set, except for the last
|
||||||
|
// byte
|
||||||
|
return readHighTagNumber();
|
||||||
|
} else {
|
||||||
|
// low-tag-number form
|
||||||
|
return tagNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readHighTagNumber() throws BerDataValueFormatException {
|
||||||
|
// Base-128 big-endian form, where each byte has the highest bit set, except for the last
|
||||||
|
// byte
|
||||||
|
int b;
|
||||||
|
int result = 0;
|
||||||
|
do {
|
||||||
|
if (!mBuf.hasRemaining()) {
|
||||||
|
throw new BerDataValueFormatException("Truncated tag number");
|
||||||
|
}
|
||||||
|
b = mBuf.get();
|
||||||
|
if (result > Integer.MAX_VALUE >>> 7) {
|
||||||
|
throw new BerDataValueFormatException("Tag number too large");
|
||||||
|
}
|
||||||
|
result <<= 7;
|
||||||
|
result |= b & 0x7f;
|
||||||
|
} while ((b & 0x80) != 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readShortFormLength(int firstLengthByte) {
|
||||||
|
return firstLengthByte & 0x7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readLongFormLength(int firstLengthByte) throws BerDataValueFormatException {
|
||||||
|
// The low 7 bits of the first byte represent the number of bytes (following the first
|
||||||
|
// byte) in which the length is in big-endian base-256 form
|
||||||
|
int byteCount = firstLengthByte & 0x7f;
|
||||||
|
if (byteCount > 4) {
|
||||||
|
throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
|
||||||
|
}
|
||||||
|
int result = 0;
|
||||||
|
for (int i = 0; i < byteCount; i++) {
|
||||||
|
if (!mBuf.hasRemaining()) {
|
||||||
|
throw new BerDataValueFormatException("Truncated length");
|
||||||
|
}
|
||||||
|
int b = mBuf.get();
|
||||||
|
if (result > Integer.MAX_VALUE >>> 8) {
|
||||||
|
throw new BerDataValueFormatException("Length too large");
|
||||||
|
}
|
||||||
|
result <<= 8;
|
||||||
|
result |= b & 0xff;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void skipDefiniteLengthContents(int contentsLength) throws BerDataValueFormatException {
|
||||||
|
if (mBuf.remaining() < contentsLength) {
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated contents. Need: " + contentsLength + " bytes, available: "
|
||||||
|
+ mBuf.remaining());
|
||||||
|
}
|
||||||
|
mBuf.position(mBuf.position() + contentsLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int skipPrimitiveIndefiniteLengthContents() throws BerDataValueFormatException {
|
||||||
|
// Contents are terminated by 0x00 0x00
|
||||||
|
boolean prevZeroByte = false;
|
||||||
|
int bytesRead = 0;
|
||||||
|
while (true) {
|
||||||
|
if (!mBuf.hasRemaining()) {
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated indefinite-length contents: " + bytesRead + " bytes read");
|
||||||
|
|
||||||
|
}
|
||||||
|
int b = mBuf.get();
|
||||||
|
bytesRead++;
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
throw new BerDataValueFormatException("Indefinite-length contents too long");
|
||||||
|
}
|
||||||
|
if (b == 0) {
|
||||||
|
if (prevZeroByte) {
|
||||||
|
// End of contents reached -- we've read the value and its terminator 0x00 0x00
|
||||||
|
return bytesRead - 2;
|
||||||
|
}
|
||||||
|
prevZeroByte = true;
|
||||||
|
} else {
|
||||||
|
prevZeroByte = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int skipConstructedIndefiniteLengthContents() throws BerDataValueFormatException {
|
||||||
|
// Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
|
||||||
|
// can contain data values which are themselves indefinite length encoded. As a result, we
|
||||||
|
// must parse the direct children of this data value to correctly skip over the contents of
|
||||||
|
// this data value.
|
||||||
|
int startPos = mBuf.position();
|
||||||
|
while (mBuf.hasRemaining()) {
|
||||||
|
// Check whether the 0x00 0x00 terminator is at current position
|
||||||
|
if ((mBuf.remaining() > 1) && (mBuf.getShort(mBuf.position()) == 0)) {
|
||||||
|
int contentsLength = mBuf.position() - startPos;
|
||||||
|
mBuf.position(mBuf.position() + 2);
|
||||||
|
return contentsLength;
|
||||||
|
}
|
||||||
|
// No luck. This must be a BER-encoded data value -- skip over it by parsing it
|
||||||
|
readDataValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated indefinite-length contents: "
|
||||||
|
+ (mBuf.position() - startPos) + " bytes read");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,313 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data
|
||||||
|
* values. See {@code X.690} for the encoding.
|
||||||
|
*/
|
||||||
|
public class InputStreamBerDataValueReader implements BerDataValueReader {
|
||||||
|
private final InputStream mIn;
|
||||||
|
|
||||||
|
public InputStreamBerDataValueReader(InputStream in) {
|
||||||
|
if (in == null) {
|
||||||
|
throw new NullPointerException("in == null");
|
||||||
|
}
|
||||||
|
mIn = in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BerDataValue readDataValue() throws BerDataValueFormatException {
|
||||||
|
return readDataValue(mIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next data value or {@code null} if end of input has been reached.
|
||||||
|
*
|
||||||
|
* @throws BerDataValueFormatException if the value being read is malformed.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("resource")
|
||||||
|
private static BerDataValue readDataValue(InputStream input)
|
||||||
|
throws BerDataValueFormatException {
|
||||||
|
RecordingInputStream in = new RecordingInputStream(input);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int firstIdentifierByte = in.read();
|
||||||
|
if (firstIdentifierByte == -1) {
|
||||||
|
// End of input
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int tagNumber = readTagNumber(in, firstIdentifierByte);
|
||||||
|
|
||||||
|
int firstLengthByte = in.read();
|
||||||
|
if (firstLengthByte == -1) {
|
||||||
|
throw new BerDataValueFormatException("Missing length");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte);
|
||||||
|
int contentsLength;
|
||||||
|
int contentsOffsetInDataValue;
|
||||||
|
if ((firstLengthByte & 0x80) == 0) {
|
||||||
|
// short form length
|
||||||
|
contentsLength = readShortFormLength(firstLengthByte);
|
||||||
|
contentsOffsetInDataValue = in.getReadByteCount();
|
||||||
|
skipDefiniteLengthContents(in, contentsLength);
|
||||||
|
} else if ((firstLengthByte & 0xff) != 0x80) {
|
||||||
|
// long form length
|
||||||
|
contentsLength = readLongFormLength(in, firstLengthByte);
|
||||||
|
contentsOffsetInDataValue = in.getReadByteCount();
|
||||||
|
skipDefiniteLengthContents(in, contentsLength);
|
||||||
|
} else {
|
||||||
|
// indefinite length
|
||||||
|
contentsOffsetInDataValue = in.getReadByteCount();
|
||||||
|
contentsLength =
|
||||||
|
constructed
|
||||||
|
? skipConstructedIndefiniteLengthContents(in)
|
||||||
|
: skipPrimitiveIndefiniteLengthContents(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encoded = in.getReadBytes();
|
||||||
|
ByteBuffer encodedContents =
|
||||||
|
ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength);
|
||||||
|
return new BerDataValue(
|
||||||
|
ByteBuffer.wrap(encoded),
|
||||||
|
encodedContents,
|
||||||
|
BerEncoding.getTagClass((byte) firstIdentifierByte),
|
||||||
|
constructed,
|
||||||
|
tagNumber);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BerDataValueFormatException("Failed to read data value", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readTagNumber(InputStream in, int firstIdentifierByte)
|
||||||
|
throws IOException, BerDataValueFormatException {
|
||||||
|
int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte);
|
||||||
|
if (tagNumber == 0x1f) {
|
||||||
|
// high-tag-number form
|
||||||
|
return readHighTagNumber(in);
|
||||||
|
} else {
|
||||||
|
// low-tag-number form
|
||||||
|
return tagNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readHighTagNumber(InputStream in)
|
||||||
|
throws IOException, BerDataValueFormatException {
|
||||||
|
// Base-128 big-endian form, where each byte has the highest bit set, except for the last
|
||||||
|
// byte where the highest bit is not set
|
||||||
|
int b;
|
||||||
|
int result = 0;
|
||||||
|
do {
|
||||||
|
b = in.read();
|
||||||
|
if (b == -1) {
|
||||||
|
throw new BerDataValueFormatException("Truncated tag number");
|
||||||
|
}
|
||||||
|
if (result > Integer.MAX_VALUE >>> 7) {
|
||||||
|
throw new BerDataValueFormatException("Tag number too large");
|
||||||
|
}
|
||||||
|
result <<= 7;
|
||||||
|
result |= b & 0x7f;
|
||||||
|
} while ((b & 0x80) != 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readShortFormLength(int firstLengthByte) {
|
||||||
|
return firstLengthByte & 0x7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readLongFormLength(InputStream in, int firstLengthByte)
|
||||||
|
throws IOException, BerDataValueFormatException {
|
||||||
|
// The low 7 bits of the first byte represent the number of bytes (following the first
|
||||||
|
// byte) in which the length is in big-endian base-256 form
|
||||||
|
int byteCount = firstLengthByte & 0x7f;
|
||||||
|
if (byteCount > 4) {
|
||||||
|
throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
|
||||||
|
}
|
||||||
|
int result = 0;
|
||||||
|
for (int i = 0; i < byteCount; i++) {
|
||||||
|
int b = in.read();
|
||||||
|
if (b == -1) {
|
||||||
|
throw new BerDataValueFormatException("Truncated length");
|
||||||
|
}
|
||||||
|
if (result > Integer.MAX_VALUE >>> 8) {
|
||||||
|
throw new BerDataValueFormatException("Length too large");
|
||||||
|
}
|
||||||
|
result <<= 8;
|
||||||
|
result |= b & 0xff;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void skipDefiniteLengthContents(InputStream in, int len)
|
||||||
|
throws IOException, BerDataValueFormatException {
|
||||||
|
long bytesRead = 0;
|
||||||
|
while (len > 0) {
|
||||||
|
int skipped = (int) in.skip(len);
|
||||||
|
if (skipped <= 0) {
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated definite-length contents: " + bytesRead + " bytes read"
|
||||||
|
+ ", " + len + " missing");
|
||||||
|
}
|
||||||
|
len -= skipped;
|
||||||
|
bytesRead += skipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int skipPrimitiveIndefiniteLengthContents(InputStream in)
|
||||||
|
throws IOException, BerDataValueFormatException {
|
||||||
|
// Contents are terminated by 0x00 0x00
|
||||||
|
boolean prevZeroByte = false;
|
||||||
|
int bytesRead = 0;
|
||||||
|
while (true) {
|
||||||
|
int b = in.read();
|
||||||
|
if (b == -1) {
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated indefinite-length contents: " + bytesRead + " bytes read");
|
||||||
|
}
|
||||||
|
bytesRead++;
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
throw new BerDataValueFormatException("Indefinite-length contents too long");
|
||||||
|
}
|
||||||
|
if (b == 0) {
|
||||||
|
if (prevZeroByte) {
|
||||||
|
// End of contents reached -- we've read the value and its terminator 0x00 0x00
|
||||||
|
return bytesRead - 2;
|
||||||
|
}
|
||||||
|
prevZeroByte = true;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
prevZeroByte = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int skipConstructedIndefiniteLengthContents(RecordingInputStream in)
|
||||||
|
throws BerDataValueFormatException {
|
||||||
|
// Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
|
||||||
|
// can contain data values which are indefinite length encoded as well. As a result, we
|
||||||
|
// must parse the direct children of this data value to correctly skip over the contents of
|
||||||
|
// this data value.
|
||||||
|
int readByteCountBefore = in.getReadByteCount();
|
||||||
|
while (true) {
|
||||||
|
// We can't easily peek for the 0x00 0x00 terminator using the provided InputStream.
|
||||||
|
// Thus, we use the fact that 0x00 0x00 parses as a data value whose encoded form we
|
||||||
|
// then check below to see whether it's 0x00 0x00.
|
||||||
|
BerDataValue dataValue = readDataValue(in);
|
||||||
|
if (dataValue == null) {
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated indefinite-length contents: "
|
||||||
|
+ (in.getReadByteCount() - readByteCountBefore) + " bytes read");
|
||||||
|
}
|
||||||
|
if (in.getReadByteCount() <= 0) {
|
||||||
|
throw new BerDataValueFormatException("Indefinite-length contents too long");
|
||||||
|
}
|
||||||
|
ByteBuffer encoded = dataValue.getEncoded();
|
||||||
|
if ((encoded.remaining() == 2) && (encoded.get(0) == 0) && (encoded.get(1) == 0)) {
|
||||||
|
// 0x00 0x00 encountered
|
||||||
|
return in.getReadByteCount() - readByteCountBefore - 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RecordingInputStream extends InputStream {
|
||||||
|
private final InputStream mIn;
|
||||||
|
private final ByteArrayOutputStream mBuf;
|
||||||
|
|
||||||
|
private RecordingInputStream(InputStream in) {
|
||||||
|
mIn = in;
|
||||||
|
mBuf = new ByteArrayOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getReadBytes() {
|
||||||
|
return mBuf.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getReadByteCount() {
|
||||||
|
return mBuf.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
int b = mIn.read();
|
||||||
|
if (b != -1) {
|
||||||
|
mBuf.write(b);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b) throws IOException {
|
||||||
|
int len = mIn.read(b);
|
||||||
|
if (len > 0) {
|
||||||
|
mBuf.write(b, 0, len);
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
len = mIn.read(b, off, len);
|
||||||
|
if (len > 0) {
|
||||||
|
mBuf.write(b, off, len);
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
if (n <= 0) {
|
||||||
|
return mIn.skip(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
int len = mIn.read(buf, 0, (int) Math.min(buf.length, n));
|
||||||
|
if (len > 0) {
|
||||||
|
mBuf.write(buf, 0, len);
|
||||||
|
}
|
||||||
|
return (len < 0) ? 0 : len;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return super.available();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void mark(int readlimit) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void reset() throws IOException {
|
||||||
|
throw new IOException("mark/reset not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,363 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.jar;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR manifest and signature file parser.
|
||||||
|
*
|
||||||
|
* <p>These files consist of a main section followed by individual sections. Individual sections
|
||||||
|
* are named, their names referring to JAR entries.
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
||||||
|
*/
|
||||||
|
public class ManifestParser {
|
||||||
|
|
||||||
|
private final byte[] mManifest;
|
||||||
|
private int mOffset;
|
||||||
|
private int mEndOffset;
|
||||||
|
|
||||||
|
private byte[] mBufferedLine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ManifestParser} with the provided input.
|
||||||
|
*/
|
||||||
|
public ManifestParser(byte[] data) {
|
||||||
|
this(data, 0, data.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ManifestParser} with the provided input.
|
||||||
|
*/
|
||||||
|
public ManifestParser(byte[] data, int offset, int length) {
|
||||||
|
mManifest = data;
|
||||||
|
mOffset = offset;
|
||||||
|
mEndOffset = offset + length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the remaining sections of this file.
|
||||||
|
*/
|
||||||
|
public List<Section> readAllSections() {
|
||||||
|
List<Section> sections = new ArrayList<>();
|
||||||
|
Section section;
|
||||||
|
while ((section = readSection()) != null) {
|
||||||
|
sections.add(section);
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next section from this file or {@code null} if end of file has been reached.
|
||||||
|
*/
|
||||||
|
public Section readSection() {
|
||||||
|
// Locate the first non-empty line
|
||||||
|
int sectionStartOffset;
|
||||||
|
String attr;
|
||||||
|
do {
|
||||||
|
sectionStartOffset = mOffset;
|
||||||
|
attr = readAttribute();
|
||||||
|
if (attr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} while (attr.length() == 0);
|
||||||
|
List<Attribute> attrs = new ArrayList<>();
|
||||||
|
attrs.add(parseAttr(attr));
|
||||||
|
|
||||||
|
// Read attributes until end of section reached
|
||||||
|
while (true) {
|
||||||
|
attr = readAttribute();
|
||||||
|
if ((attr == null) || (attr.length() == 0)) {
|
||||||
|
// End of section
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
attrs.add(parseAttr(attr));
|
||||||
|
}
|
||||||
|
|
||||||
|
int sectionEndOffset = mOffset;
|
||||||
|
int sectionSizeBytes = sectionEndOffset - sectionStartOffset;
|
||||||
|
|
||||||
|
return new Section(sectionStartOffset, sectionSizeBytes, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Attribute parseAttr(String attr) {
|
||||||
|
// Name is separated from value by a semicolon followed by a single SPACE character.
|
||||||
|
// This permits trailing spaces in names and leading and trailing spaces in values.
|
||||||
|
// Some APK obfuscators take advantage of this fact. We thus need to preserve these unusual
|
||||||
|
// spaces to be able to parse such obfuscated APKs.
|
||||||
|
int delimiterIndex = attr.indexOf(": ");
|
||||||
|
if (delimiterIndex == -1) {
|
||||||
|
return new Attribute(attr, "");
|
||||||
|
} else {
|
||||||
|
return new Attribute(
|
||||||
|
attr.substring(0, delimiterIndex),
|
||||||
|
attr.substring(delimiterIndex + ": ".length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next attribute or empty {@code String} if end of section has been reached or
|
||||||
|
* {@code null} if end of input has been reached.
|
||||||
|
*/
|
||||||
|
private String readAttribute() {
|
||||||
|
byte[] bytes = readAttributeBytes();
|
||||||
|
if (bytes == null) {
|
||||||
|
return null;
|
||||||
|
} else if (bytes.length == 0) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return new String(bytes, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next attribute or empty array if end of section has been reached or {@code null}
|
||||||
|
* if end of input has been reached.
|
||||||
|
*/
|
||||||
|
private byte[] readAttributeBytes() {
|
||||||
|
// Check whether end of section was reached during previous invocation
|
||||||
|
if ((mBufferedLine != null) && (mBufferedLine.length == 0)) {
|
||||||
|
mBufferedLine = null;
|
||||||
|
return EMPTY_BYTE_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the next line
|
||||||
|
byte[] line = readLine();
|
||||||
|
if (line == null) {
|
||||||
|
// End of input
|
||||||
|
if (mBufferedLine != null) {
|
||||||
|
byte[] result = mBufferedLine;
|
||||||
|
mBufferedLine = null;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume the read line
|
||||||
|
if (line.length == 0) {
|
||||||
|
// End of section
|
||||||
|
if (mBufferedLine != null) {
|
||||||
|
byte[] result = mBufferedLine;
|
||||||
|
mBufferedLine = EMPTY_BYTE_ARRAY;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return EMPTY_BYTE_ARRAY;
|
||||||
|
}
|
||||||
|
byte[] attrLine;
|
||||||
|
if (mBufferedLine == null) {
|
||||||
|
attrLine = line;
|
||||||
|
} else {
|
||||||
|
if ((line.length == 0) || (line[0] != ' ')) {
|
||||||
|
// The most common case: buffered line is a full attribute
|
||||||
|
byte[] result = mBufferedLine;
|
||||||
|
mBufferedLine = line;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
attrLine = mBufferedLine;
|
||||||
|
mBufferedLine = null;
|
||||||
|
attrLine = concat(attrLine, line, 1, line.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything's buffered in attrLine now. mBufferedLine is null
|
||||||
|
|
||||||
|
// Read more lines
|
||||||
|
while (true) {
|
||||||
|
line = readLine();
|
||||||
|
if (line == null) {
|
||||||
|
// End of input
|
||||||
|
return attrLine;
|
||||||
|
} else if (line.length == 0) {
|
||||||
|
// End of section
|
||||||
|
mBufferedLine = EMPTY_BYTE_ARRAY; // return "end of section" next time
|
||||||
|
return attrLine;
|
||||||
|
}
|
||||||
|
if (line[0] == ' ') {
|
||||||
|
// Continuation line
|
||||||
|
attrLine = concat(attrLine, line, 1, line.length - 1);
|
||||||
|
} else {
|
||||||
|
// Next attribute
|
||||||
|
mBufferedLine = line;
|
||||||
|
return attrLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||||
|
|
||||||
|
private static byte[] concat(byte[] arr1, byte[] arr2, int offset2, int length2) {
|
||||||
|
byte[] result = new byte[arr1.length + length2];
|
||||||
|
System.arraycopy(arr1, 0, result, 0, arr1.length);
|
||||||
|
System.arraycopy(arr2, offset2, result, arr1.length, length2);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next line (without line delimiter characters) or {@code null} if end of input has
|
||||||
|
* been reached.
|
||||||
|
*/
|
||||||
|
private byte[] readLine() {
|
||||||
|
if (mOffset >= mEndOffset) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int startOffset = mOffset;
|
||||||
|
int newlineStartOffset = -1;
|
||||||
|
int newlineEndOffset = -1;
|
||||||
|
for (int i = startOffset; i < mEndOffset; i++) {
|
||||||
|
byte b = mManifest[i];
|
||||||
|
if (b == '\r') {
|
||||||
|
newlineStartOffset = i;
|
||||||
|
int nextIndex = i + 1;
|
||||||
|
if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) {
|
||||||
|
newlineEndOffset = nextIndex + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
newlineEndOffset = nextIndex;
|
||||||
|
break;
|
||||||
|
} else if (b == '\n') {
|
||||||
|
newlineStartOffset = i;
|
||||||
|
newlineEndOffset = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newlineStartOffset == -1) {
|
||||||
|
newlineStartOffset = mEndOffset;
|
||||||
|
newlineEndOffset = mEndOffset;
|
||||||
|
}
|
||||||
|
mOffset = newlineEndOffset;
|
||||||
|
|
||||||
|
if (newlineStartOffset == startOffset) {
|
||||||
|
return EMPTY_BYTE_ARRAY;
|
||||||
|
}
|
||||||
|
return Arrays.copyOfRange(mManifest, startOffset, newlineStartOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute.
|
||||||
|
*/
|
||||||
|
public static class Attribute {
|
||||||
|
private final String mName;
|
||||||
|
private final String mValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Attribute} with the provided name and value.
|
||||||
|
*/
|
||||||
|
public Attribute(String name, String value) {
|
||||||
|
mName = name;
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this attribute's name.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this attribute's value.
|
||||||
|
*/
|
||||||
|
public String getValue() {
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section.
|
||||||
|
*/
|
||||||
|
public static class Section {
|
||||||
|
private final int mStartOffset;
|
||||||
|
private final int mSizeBytes;
|
||||||
|
private final String mName;
|
||||||
|
private final List<Attribute> mAttributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Section}.
|
||||||
|
*
|
||||||
|
* @param startOffset start offset (in bytes) of the section in the input file
|
||||||
|
* @param sizeBytes size (in bytes) of the section in the input file
|
||||||
|
* @param attrs attributes contained in the section
|
||||||
|
*/
|
||||||
|
public Section(int startOffset, int sizeBytes, List<Attribute> attrs) {
|
||||||
|
mStartOffset = startOffset;
|
||||||
|
mSizeBytes = sizeBytes;
|
||||||
|
String sectionName = null;
|
||||||
|
if (!attrs.isEmpty()) {
|
||||||
|
Attribute firstAttr = attrs.get(0);
|
||||||
|
if ("Name".equalsIgnoreCase(firstAttr.getName())) {
|
||||||
|
sectionName = firstAttr.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mName = sectionName;
|
||||||
|
mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the offset (in bytes) at which this section starts in the input.
|
||||||
|
*/
|
||||||
|
public int getStartOffset() {
|
||||||
|
return mStartOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size (in bytes) of this section in the input.
|
||||||
|
*/
|
||||||
|
public int getSizeBytes() {
|
||||||
|
return mSizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this section's attributes, in the order in which they appear in the input.
|
||||||
|
*/
|
||||||
|
public List<Attribute> getAttributes() {
|
||||||
|
return mAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the specified attribute in this section or {@code null} if this
|
||||||
|
* section does not contain a matching attribute.
|
||||||
|
*/
|
||||||
|
public String getAttributeValue(Attributes.Name name) {
|
||||||
|
return getAttributeValue(name.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the specified attribute in this section or {@code null} if this
|
||||||
|
* section does not contain a matching attribute.
|
||||||
|
*
|
||||||
|
* @param name name of the attribute. Attribute names are case-insensitive.
|
||||||
|
*/
|
||||||
|
public String getAttributeValue(String name) {
|
||||||
|
for (Attribute attr : mAttributes) {
|
||||||
|
if (attr.getName().equalsIgnoreCase(name)) {
|
||||||
|
return attr.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Producer of {@code META-INF/MANIFEST.MF} file.
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
||||||
|
*/
|
||||||
|
public abstract class ManifestWriter {
|
||||||
|
|
||||||
|
private static final byte[] CRLF = new byte[] {'\r', '\n'};
|
||||||
|
private static final int MAX_LINE_LENGTH = 70;
|
||||||
|
|
||||||
|
private ManifestWriter() {}
|
||||||
|
|
||||||
|
public static void writeMainSection(OutputStream out, Attributes attributes)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
// Main section must start with the Manifest-Version attribute.
|
||||||
|
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
|
||||||
|
String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION);
|
||||||
|
if (manifestVersion == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing");
|
||||||
|
}
|
||||||
|
writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion);
|
||||||
|
|
||||||
|
if (attributes.size() > 1) {
|
||||||
|
SortedMap<String, String> namedAttributes = getAttributesSortedByName(attributes);
|
||||||
|
namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString());
|
||||||
|
writeAttributes(out, namedAttributes);
|
||||||
|
}
|
||||||
|
writeSectionDelimiter(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
|
||||||
|
throws IOException {
|
||||||
|
writeAttribute(out, "Name", name);
|
||||||
|
|
||||||
|
if (!attributes.isEmpty()) {
|
||||||
|
writeAttributes(out, getAttributesSortedByName(attributes));
|
||||||
|
}
|
||||||
|
writeSectionDelimiter(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeSectionDelimiter(OutputStream out) throws IOException {
|
||||||
|
out.write(CRLF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeAttribute(OutputStream out, Attributes.Name name, String value)
|
||||||
|
throws IOException {
|
||||||
|
writeAttribute(out, name.toString(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeAttribute(OutputStream out, String name, String value)
|
||||||
|
throws IOException {
|
||||||
|
writeLine(out, name + ": " + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeLine(OutputStream out, String line) throws IOException {
|
||||||
|
byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8);
|
||||||
|
int offset = 0;
|
||||||
|
int remaining = lineBytes.length;
|
||||||
|
boolean firstLine = true;
|
||||||
|
while (remaining > 0) {
|
||||||
|
int chunkLength;
|
||||||
|
if (firstLine) {
|
||||||
|
// First line
|
||||||
|
chunkLength = Math.min(remaining, MAX_LINE_LENGTH);
|
||||||
|
} else {
|
||||||
|
// Continuation line
|
||||||
|
out.write(CRLF);
|
||||||
|
out.write(' ');
|
||||||
|
chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1);
|
||||||
|
}
|
||||||
|
out.write(lineBytes, offset, chunkLength);
|
||||||
|
offset += chunkLength;
|
||||||
|
remaining -= chunkLength;
|
||||||
|
firstLine = false;
|
||||||
|
}
|
||||||
|
out.write(CRLF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SortedMap<String, String> getAttributesSortedByName(Attributes attributes) {
|
||||||
|
Set<Map.Entry<Object, Object>> attributesEntries = attributes.entrySet();
|
||||||
|
SortedMap<String, String> namedAttributes = new TreeMap<String, String>();
|
||||||
|
for (Map.Entry<Object, Object> attribute : attributesEntries) {
|
||||||
|
String attrName = attribute.getKey().toString();
|
||||||
|
String attrValue = attribute.getValue().toString();
|
||||||
|
namedAttributes.put(attrName, attrValue);
|
||||||
|
}
|
||||||
|
return namedAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeAttributes(
|
||||||
|
OutputStream out, SortedMap<String, String> attributesSortedByName) throws IOException {
|
||||||
|
for (Map.Entry<String, String> attribute : attributesSortedByName.entrySet()) {
|
||||||
|
String attrName = attribute.getKey();
|
||||||
|
String attrValue = attribute.getValue();
|
||||||
|
writeAttribute(out, attrName, attrValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Producer of JAR signature file ({@code *.SF}).
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
||||||
|
*/
|
||||||
|
public abstract class SignatureFileWriter {
|
||||||
|
private SignatureFileWriter() {}
|
||||||
|
|
||||||
|
public static void writeMainSection(OutputStream out, Attributes attributes)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
// Main section must start with the Signature-Version attribute.
|
||||||
|
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
|
||||||
|
String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION);
|
||||||
|
if (signatureVersion == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing");
|
||||||
|
}
|
||||||
|
ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion);
|
||||||
|
|
||||||
|
if (attributes.size() > 1) {
|
||||||
|
SortedMap<String, String> namedAttributes =
|
||||||
|
ManifestWriter.getAttributesSortedByName(attributes);
|
||||||
|
namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString());
|
||||||
|
ManifestWriter.writeAttributes(out, namedAttributes);
|
||||||
|
}
|
||||||
|
writeSectionDelimiter(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
|
||||||
|
throws IOException {
|
||||||
|
ManifestWriter.writeIndividualSection(out, name, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeSectionDelimiter(OutputStream out) throws IOException {
|
||||||
|
ManifestWriter.writeSectionDelimiter(out);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class AlgorithmIdentifier {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||||
|
public String algorithm;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.ANY, optional = true)
|
||||||
|
public Asn1OpaqueObject parameters;
|
||||||
|
|
||||||
|
public AlgorithmIdentifier() {}
|
||||||
|
|
||||||
|
public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) {
|
||||||
|
this.algorithm = algorithmOid;
|
||||||
|
this.parameters = parameters;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code Attribute} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class Attribute {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||||
|
public String attrType;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.SET_OF)
|
||||||
|
public List<Asn1OpaqueObject> attrValues;
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code ContentInfo} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class ContentInfo {
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||||
|
public String contentType;
|
||||||
|
|
||||||
|
@Asn1Field(index = 2, type = Asn1Type.ANY, tagging = Asn1Tagging.EXPLICIT, tagNumber = 0)
|
||||||
|
public Asn1OpaqueObject content;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code EncapsulatedContentInfo} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class EncapsulatedContentInfo {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||||
|
public String contentType;
|
||||||
|
|
||||||
|
@Asn1Field(
|
||||||
|
index = 1,
|
||||||
|
type = Asn1Type.OCTET_STRING,
|
||||||
|
tagging = Asn1Tagging.EXPLICIT, tagNumber = 0,
|
||||||
|
optional = true)
|
||||||
|
public ByteBuffer content;
|
||||||
|
|
||||||
|
public EncapsulatedContentInfo() {}
|
||||||
|
|
||||||
|
public EncapsulatedContentInfo(String contentTypeOid) {
|
||||||
|
contentType = contentTypeOid;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code IssuerAndSerialNumber} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class IssuerAndSerialNumber {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.ANY)
|
||||||
|
public Asn1OpaqueObject issuer;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.INTEGER)
|
||||||
|
public BigInteger certificateSerialNumber;
|
||||||
|
|
||||||
|
public IssuerAndSerialNumber() {}
|
||||||
|
|
||||||
|
public IssuerAndSerialNumber(Asn1OpaqueObject issuer, BigInteger certificateSerialNumber) {
|
||||||
|
this.issuer = issuer;
|
||||||
|
this.certificateSerialNumber = certificateSerialNumber;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assorted PKCS #7 constants from RFC 5652.
|
||||||
|
*/
|
||||||
|
public abstract class Pkcs7Constants {
|
||||||
|
private Pkcs7Constants() {}
|
||||||
|
|
||||||
|
public static final String OID_DATA = "1.2.840.113549.1.7.1";
|
||||||
|
public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2";
|
||||||
|
public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3";
|
||||||
|
public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an error was encountered while decoding a PKCS #7 structure.
|
||||||
|
*/
|
||||||
|
public class Pkcs7DecodingException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public Pkcs7DecodingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pkcs7DecodingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code SignedData} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class SignedData {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.INTEGER)
|
||||||
|
public int version;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.SET_OF)
|
||||||
|
public List<AlgorithmIdentifier> digestAlgorithms;
|
||||||
|
|
||||||
|
@Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
|
||||||
|
public EncapsulatedContentInfo encapContentInfo;
|
||||||
|
|
||||||
|
@Asn1Field(
|
||||||
|
index = 3,
|
||||||
|
type = Asn1Type.SET_OF,
|
||||||
|
tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
|
||||||
|
optional = true)
|
||||||
|
public List<Asn1OpaqueObject> certificates;
|
||||||
|
|
||||||
|
@Asn1Field(
|
||||||
|
index = 4,
|
||||||
|
type = Asn1Type.SET_OF,
|
||||||
|
tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
|
||||||
|
optional = true)
|
||||||
|
public List<ByteBuffer> crls;
|
||||||
|
|
||||||
|
@Asn1Field(index = 5, type = Asn1Type.SET_OF)
|
||||||
|
public List<SignerInfo> signerInfos;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code SignerIdentifier} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.CHOICE)
|
||||||
|
public class SignerIdentifier {
|
||||||
|
|
||||||
|
@Asn1Field(type = Asn1Type.SEQUENCE)
|
||||||
|
public IssuerAndSerialNumber issuerAndSerialNumber;
|
||||||
|
|
||||||
|
@Asn1Field(type = Asn1Type.OCTET_STRING, tagging = Asn1Tagging.IMPLICIT, tagNumber = 0)
|
||||||
|
public ByteBuffer subjectKeyIdentifier;
|
||||||
|
|
||||||
|
public SignerIdentifier() {}
|
||||||
|
|
||||||
|
public SignerIdentifier(IssuerAndSerialNumber issuerAndSerialNumber) {
|
||||||
|
this.issuerAndSerialNumber = issuerAndSerialNumber;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code SignerInfo} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class SignerInfo {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.INTEGER)
|
||||||
|
public int version;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.CHOICE)
|
||||||
|
public SignerIdentifier sid;
|
||||||
|
|
||||||
|
@Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
|
||||||
|
public AlgorithmIdentifier digestAlgorithm;
|
||||||
|
|
||||||
|
@Asn1Field(
|
||||||
|
index = 3,
|
||||||
|
type = Asn1Type.SET_OF,
|
||||||
|
tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
|
||||||
|
optional = true)
|
||||||
|
public Asn1OpaqueObject signedAttrs;
|
||||||
|
|
||||||
|
@Asn1Field(index = 4, type = Asn1Type.SEQUENCE)
|
||||||
|
public AlgorithmIdentifier signatureAlgorithm;
|
||||||
|
|
||||||
|
@Asn1Field(index = 5, type = Asn1Type.OCTET_STRING)
|
||||||
|
public ByteBuffer signature;
|
||||||
|
|
||||||
|
@Asn1Field(
|
||||||
|
index = 6,
|
||||||
|
type = Asn1Type.SET_OF,
|
||||||
|
tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
|
||||||
|
optional = true)
|
||||||
|
public List<Attribute> unsignedAttrs;
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android SDK version / API Level constants.
|
||||||
|
*/
|
||||||
|
public abstract class AndroidSdkVersion {
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private AndroidSdkVersion() {}
|
||||||
|
|
||||||
|
/** Android 2.3. */
|
||||||
|
public static final int GINGERBREAD = 9;
|
||||||
|
|
||||||
|
/** Android 4.3. The revenge of the beans. */
|
||||||
|
public static final int JELLY_BEAN_MR2 = 18;
|
||||||
|
|
||||||
|
/** Android 4.4. KitKat, another tasty treat. */
|
||||||
|
public static final int KITKAT = 19;
|
||||||
|
|
||||||
|
/** Android 5.0. A flat one with beautiful shadows. But still tasty. */
|
||||||
|
public static final int LOLLIPOP = 21;
|
||||||
|
|
||||||
|
/** Android 6.0. M is for Marshmallow! */
|
||||||
|
public static final int M = 23;
|
||||||
|
|
||||||
|
/** Android 7.0. N is for Nougat. */
|
||||||
|
public static final int N = 24;
|
||||||
|
|
||||||
|
/** Android O. */
|
||||||
|
public static final int O = 26;
|
||||||
|
|
||||||
|
/** Android P. */
|
||||||
|
public static final int P = 28;
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.ReadableDataSink;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Growable byte array which can be appended to via {@link DataSink} interface and read from via
|
||||||
|
* {@link DataSource} interface.
|
||||||
|
*/
|
||||||
|
public class ByteArrayDataSink implements ReadableDataSink {
|
||||||
|
|
||||||
|
private static final int MAX_READ_CHUNK_SIZE = 65536;
|
||||||
|
|
||||||
|
private byte[] mArray;
|
||||||
|
private int mSize;
|
||||||
|
|
||||||
|
public ByteArrayDataSink() {
|
||||||
|
this(65536);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteArrayDataSink(int initialCapacity) {
|
||||||
|
if (initialCapacity < 0) {
|
||||||
|
throw new IllegalArgumentException("initial capacity: " + initialCapacity);
|
||||||
|
}
|
||||||
|
mArray = new byte[initialCapacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||||
|
if (offset < 0) {
|
||||||
|
// Must perform this check because System.arraycopy below doesn't perform it when
|
||||||
|
// length == 0
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (offset > buf.length) {
|
||||||
|
// Must perform this check because System.arraycopy below doesn't perform it when
|
||||||
|
// length == 0
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset: " + offset + ", buf.length: " + buf.length);
|
||||||
|
}
|
||||||
|
if (length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAvailable(length);
|
||||||
|
System.arraycopy(buf, offset, mArray, mSize, length);
|
||||||
|
mSize += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) throws IOException {
|
||||||
|
if (!buf.hasRemaining()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.hasArray()) {
|
||||||
|
consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
|
||||||
|
buf.position(buf.limit());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAvailable(buf.remaining());
|
||||||
|
byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)];
|
||||||
|
while (buf.hasRemaining()) {
|
||||||
|
int chunkSize = Math.min(buf.remaining(), tmp.length);
|
||||||
|
buf.get(tmp, 0, chunkSize);
|
||||||
|
System.arraycopy(tmp, 0, mArray, mSize, chunkSize);
|
||||||
|
mSize += chunkSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureAvailable(int minAvailable) throws IOException {
|
||||||
|
if (minAvailable <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long minCapacity = ((long) mSize) + minAvailable;
|
||||||
|
if (minCapacity <= mArray.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (minCapacity > Integer.MAX_VALUE) {
|
||||||
|
throw new IOException(
|
||||||
|
"Required capacity too large: " + minCapacity + ", max: " + Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
int doubleCurrentSize = (int) Math.min(mArray.length * 2L, Integer.MAX_VALUE);
|
||||||
|
int newSize = (int) Math.max(minCapacity, doubleCurrentSize);
|
||||||
|
mArray = Arrays.copyOf(mArray, newSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
return mSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
|
||||||
|
// checkChunkValid ensures that it's OK to cast offset to int.
|
||||||
|
return ByteBuffer.wrap(mArray, (int) offset, size).slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
|
||||||
|
// checkChunkValid ensures that it's OK to cast offset and size to int.
|
||||||
|
sink.consume(mArray, (int) offset, (int) size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
|
||||||
|
// checkChunkValid ensures that it's OK to cast offset to int.
|
||||||
|
dest.put(mArray, (int) offset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkChunkValid(long offset, long size) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
if (offset > mSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") > source size (" + mSize + ")");
|
||||||
|
}
|
||||||
|
long endOffset = offset + size;
|
||||||
|
if (endOffset < offset) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||||
|
}
|
||||||
|
if (endOffset > mSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataSource slice(long offset, long size) {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
// checkChunkValid ensures that it's OK to cast offset and size to int.
|
||||||
|
return new SliceDataSource((int) offset, (int) size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slice of the growable byte array. The slice's offset and size in the array are fixed.
|
||||||
|
*/
|
||||||
|
private class SliceDataSource implements DataSource {
|
||||||
|
private final int mSliceOffset;
|
||||||
|
private final int mSliceSize;
|
||||||
|
|
||||||
|
private SliceDataSource(int offset, int size) {
|
||||||
|
mSliceOffset = offset;
|
||||||
|
mSliceSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
return mSliceSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||||
|
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
|
||||||
|
sink.consume(mArray, (int) (mSliceOffset + offset), (int) size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||||
|
// that mSliceOffset + offset does not overflow.
|
||||||
|
return ByteBuffer.wrap(mArray, (int) (mSliceOffset + offset), size).slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||||
|
// that mSliceOffset + offset does not overflow.
|
||||||
|
dest.put(mArray, (int) (mSliceOffset + offset), size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataSource slice(long offset, long size) {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||||
|
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
|
||||||
|
return new SliceDataSource((int) (mSliceOffset + offset), (int) size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkChunkValid(long offset, long size) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
if (offset > mSliceSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") > source size (" + mSliceSize + ")");
|
||||||
|
}
|
||||||
|
long endOffset = offset + size;
|
||||||
|
if (endOffset < offset) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||||
|
}
|
||||||
|
if (endOffset > mSliceSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") > source size (" + mSliceSize
|
||||||
|
+ ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataSource} backed by a {@link ByteBuffer}.
|
||||||
|
*/
|
||||||
|
public class ByteBufferDataSource implements DataSource {
|
||||||
|
|
||||||
|
private final ByteBuffer mBuffer;
|
||||||
|
private final int mSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
|
||||||
|
* buffer between the buffer's position and limit.
|
||||||
|
*/
|
||||||
|
public ByteBufferDataSource(ByteBuffer buffer) {
|
||||||
|
this(buffer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
|
||||||
|
* buffer between the buffer's position and limit.
|
||||||
|
*/
|
||||||
|
private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) {
|
||||||
|
mBuffer = (sliceRequired) ? buffer.slice() : buffer;
|
||||||
|
mSize = buffer.remaining();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
return mSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
|
||||||
|
// checkChunkValid ensures that it's OK to cast offset to int.
|
||||||
|
int chunkPosition = (int) offset;
|
||||||
|
int chunkLimit = chunkPosition + size;
|
||||||
|
// Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
|
||||||
|
// and limit fields, to be more specific). We thus use synchronization around these
|
||||||
|
// state-changing operations to make instances of this class thread-safe.
|
||||||
|
synchronized (mBuffer) {
|
||||||
|
// ByteBuffer.limit(int) and .position(int) check that that the position >= limit
|
||||||
|
// invariant is not broken. Thus, the only way to safely change position and limit
|
||||||
|
// without caring about their current values is to first set position to 0 or set the
|
||||||
|
// limit to capacity.
|
||||||
|
mBuffer.position(0);
|
||||||
|
|
||||||
|
mBuffer.limit(chunkLimit);
|
||||||
|
mBuffer.position(chunkPosition);
|
||||||
|
return mBuffer.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyTo(long offset, int size, ByteBuffer dest) {
|
||||||
|
dest.put(getByteBuffer(offset, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||||
|
if ((size < 0) || (size > mSize)) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
|
||||||
|
}
|
||||||
|
sink.consume(getByteBuffer(offset, (int) size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBufferDataSource slice(long offset, long size) {
|
||||||
|
if ((offset == 0) && (size == mSize)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if ((size < 0) || (size > mSize)) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
|
||||||
|
}
|
||||||
|
return new ByteBufferDataSource(
|
||||||
|
getByteBuffer(offset, (int) size),
|
||||||
|
false // no need to slice -- it's already a slice
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkChunkValid(long offset, long size) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
if (offset > mSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") > source size (" + mSize + ")");
|
||||||
|
}
|
||||||
|
long endOffset = offset + size;
|
||||||
|
if (endOffset < offset) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||||
|
}
|
||||||
|
if (endOffset > mSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferOverflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data sink which stores all received data into the associated {@link ByteBuffer}.
|
||||||
|
*/
|
||||||
|
public class ByteBufferSink implements DataSink {
|
||||||
|
|
||||||
|
private final ByteBuffer mBuffer;
|
||||||
|
|
||||||
|
public ByteBufferSink(ByteBuffer buffer) {
|
||||||
|
mBuffer = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getBuffer() {
|
||||||
|
return mBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||||
|
try {
|
||||||
|
mBuffer.put(buf, offset, length);
|
||||||
|
} catch (BufferOverflowException e) {
|
||||||
|
throw new IOException(
|
||||||
|
"Insufficient space in output buffer for " + length + " bytes", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) throws IOException {
|
||||||
|
int length = buf.remaining();
|
||||||
|
try {
|
||||||
|
mBuffer.put(buf);
|
||||||
|
} catch (BufferOverflowException e) {
|
||||||
|
throw new IOException(
|
||||||
|
"Insufficient space in output buffer for " + length + " bytes", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public final class ByteBufferUtils {
|
||||||
|
private ByteBufferUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the remaining data of the provided buffer as a new byte array and advances the
|
||||||
|
* position of the buffer to the buffer's limit.
|
||||||
|
*/
|
||||||
|
public static byte[] toByteArray(ByteBuffer buf) {
|
||||||
|
byte[] result = new byte[buf.remaining()];
|
||||||
|
buf.get(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for byte arrays and I/O streams.
|
||||||
|
*/
|
||||||
|
public final class ByteStreams {
|
||||||
|
private ByteStreams() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data remaining in the provided input stream as a byte array
|
||||||
|
*/
|
||||||
|
public static byte[] toByteArray(InputStream in) throws IOException {
|
||||||
|
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||||
|
byte[] buf = new byte[16384];
|
||||||
|
int chunkSize;
|
||||||
|
while ((chunkSize = in.read(buf)) != -1) {
|
||||||
|
result.write(buf, 0, chunkSize);
|
||||||
|
}
|
||||||
|
return result.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/** Pseudo {@link DataSource} that chains the given {@link DataSource} as a continuous one. */
|
||||||
|
public class ChainedDataSource implements DataSource {
|
||||||
|
|
||||||
|
private final DataSource[] mSources;
|
||||||
|
private final long mTotalSize;
|
||||||
|
|
||||||
|
public ChainedDataSource(DataSource... sources) {
|
||||||
|
mSources = sources;
|
||||||
|
mTotalSize = Arrays.stream(sources).mapToLong(src -> src.size()).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
return mTotalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||||
|
if (offset + size > mTotalSize) {
|
||||||
|
throw new IndexOutOfBoundsException("Requested more than available");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DataSource src : mSources) {
|
||||||
|
// Offset is beyond the current source. Skip.
|
||||||
|
if (offset >= src.size()) {
|
||||||
|
offset -= src.size();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the remaining is enough, finish it.
|
||||||
|
long remaining = src.size() - offset;
|
||||||
|
if (remaining >= size) {
|
||||||
|
src.feed(offset, size, sink);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the remaining is not enough, consume all.
|
||||||
|
src.feed(offset, remaining, sink);
|
||||||
|
size -= remaining;
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
||||||
|
if (offset + size > mTotalSize) {
|
||||||
|
throw new IndexOutOfBoundsException("Requested more than available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip to the first DataSource we need.
|
||||||
|
Pair<Integer, Long> firstSource = locateDataSource(offset);
|
||||||
|
int i = firstSource.getFirst();
|
||||||
|
offset = firstSource.getSecond();
|
||||||
|
|
||||||
|
// Return the current source's ByteBuffer if it fits.
|
||||||
|
if (offset + size <= mSources[i].size()) {
|
||||||
|
return mSources[i].getByteBuffer(offset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, read into a new buffer.
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||||
|
for (; i < mSources.length && buffer.hasRemaining(); i++) {
|
||||||
|
long sizeToCopy = Math.min(mSources[i].size() - offset, buffer.remaining());
|
||||||
|
mSources[i].copyTo(offset, Math.toIntExact(sizeToCopy), buffer);
|
||||||
|
offset = 0; // may not be zero for the first source, but reset after that.
|
||||||
|
}
|
||||||
|
buffer.rewind();
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||||
|
feed(offset, size, new ByteBufferSink(dest));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataSource slice(long offset, long size) {
|
||||||
|
// Find the first slice.
|
||||||
|
Pair<Integer, Long> firstSource = locateDataSource(offset);
|
||||||
|
int beginIndex = firstSource.getFirst();
|
||||||
|
long beginLocalOffset = firstSource.getSecond();
|
||||||
|
DataSource beginSource = mSources[beginIndex];
|
||||||
|
|
||||||
|
if (beginLocalOffset + size <= beginSource.size()) {
|
||||||
|
return beginSource.slice(beginLocalOffset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the first slice to chaining, followed by the middle full slices, then the last.
|
||||||
|
ArrayList<DataSource> sources = new ArrayList<>();
|
||||||
|
sources.add(beginSource.slice(
|
||||||
|
beginLocalOffset, beginSource.size() - beginLocalOffset));
|
||||||
|
|
||||||
|
Pair<Integer, Long> lastSource = locateDataSource(offset + size - 1);
|
||||||
|
int endIndex = lastSource.getFirst();
|
||||||
|
long endLocalOffset = lastSource.getSecond();
|
||||||
|
|
||||||
|
for (int i = beginIndex + 1; i < endIndex; i++) {
|
||||||
|
sources.add(mSources[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
sources.add(mSources[endIndex].slice(0, endLocalOffset + 1));
|
||||||
|
return new ChainedDataSource(sources.toArray(new DataSource[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the index of DataSource that offset is at.
|
||||||
|
* @return Pair of DataSource index and the local offset in the DataSource.
|
||||||
|
*/
|
||||||
|
private Pair<Integer, Long> locateDataSource(long offset) {
|
||||||
|
long localOffset = offset;
|
||||||
|
for (int i = 0; i < mSources.length; i++) {
|
||||||
|
if (localOffset < mSources[i].size()) {
|
||||||
|
return Pair.of(i, localOffset);
|
||||||
|
}
|
||||||
|
localOffset -= mSources[i].size();
|
||||||
|
}
|
||||||
|
throw new IndexOutOfBoundsException("Access is out of bound, offset: " + offset +
|
||||||
|
", totalSize: " + mTotalSize);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,217 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.Provider;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateExpiredException;
|
||||||
|
import java.security.cert.CertificateNotYetValidException;
|
||||||
|
import java.security.cert.CertificateParsingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.security.auth.x500.X500Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link X509Certificate} which delegates all method invocations to the provided delegate
|
||||||
|
* {@code X509Certificate}.
|
||||||
|
*/
|
||||||
|
public class DelegatingX509Certificate extends X509Certificate {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final X509Certificate mDelegate;
|
||||||
|
|
||||||
|
public DelegatingX509Certificate(X509Certificate delegate) {
|
||||||
|
this.mDelegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getCriticalExtensionOIDs() {
|
||||||
|
return mDelegate.getCriticalExtensionOIDs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getExtensionValue(String oid) {
|
||||||
|
return mDelegate.getExtensionValue(oid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getNonCriticalExtensionOIDs() {
|
||||||
|
return mDelegate.getNonCriticalExtensionOIDs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasUnsupportedCriticalExtension() {
|
||||||
|
return mDelegate.hasUnsupportedCriticalExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkValidity()
|
||||||
|
throws CertificateExpiredException, CertificateNotYetValidException {
|
||||||
|
mDelegate.checkValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkValidity(Date date)
|
||||||
|
throws CertificateExpiredException, CertificateNotYetValidException {
|
||||||
|
mDelegate.checkValidity(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVersion() {
|
||||||
|
return mDelegate.getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigInteger getSerialNumber() {
|
||||||
|
return mDelegate.getSerialNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Principal getIssuerDN() {
|
||||||
|
return mDelegate.getIssuerDN();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Principal getSubjectDN() {
|
||||||
|
return mDelegate.getSubjectDN();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getNotBefore() {
|
||||||
|
return mDelegate.getNotBefore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getNotAfter() {
|
||||||
|
return mDelegate.getNotAfter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getTBSCertificate() throws CertificateEncodingException {
|
||||||
|
return mDelegate.getTBSCertificate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getSignature() {
|
||||||
|
return mDelegate.getSignature();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSigAlgName() {
|
||||||
|
return mDelegate.getSigAlgName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSigAlgOID() {
|
||||||
|
return mDelegate.getSigAlgOID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getSigAlgParams() {
|
||||||
|
return mDelegate.getSigAlgParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean[] getIssuerUniqueID() {
|
||||||
|
return mDelegate.getIssuerUniqueID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean[] getSubjectUniqueID() {
|
||||||
|
return mDelegate.getSubjectUniqueID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean[] getKeyUsage() {
|
||||||
|
return mDelegate.getKeyUsage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBasicConstraints() {
|
||||||
|
return mDelegate.getBasicConstraints();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getEncoded() throws CertificateEncodingException {
|
||||||
|
return mDelegate.getEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
|
||||||
|
InvalidKeyException, NoSuchProviderException, SignatureException {
|
||||||
|
mDelegate.verify(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verify(PublicKey key, String sigProvider)
|
||||||
|
throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
|
||||||
|
NoSuchProviderException, SignatureException {
|
||||||
|
mDelegate.verify(key, sigProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return mDelegate.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKey getPublicKey() {
|
||||||
|
return mDelegate.getPublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X500Principal getIssuerX500Principal() {
|
||||||
|
return mDelegate.getIssuerX500Principal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X500Principal getSubjectX500Principal() {
|
||||||
|
return mDelegate.getSubjectX500Principal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getExtendedKeyUsage() throws CertificateParsingException {
|
||||||
|
return mDelegate.getExtendedKeyUsage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException {
|
||||||
|
return mDelegate.getSubjectAlternativeNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<List<?>> getIssuerAlternativeNames() throws CertificateParsingException {
|
||||||
|
return mDelegate.getIssuerAlternativeNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verify(PublicKey key, Provider sigProvider) throws CertificateException,
|
||||||
|
NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
|
mDelegate.verify(key, sigProvider);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.BufferOverflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataSource} backed by a {@link FileChannel} for {@link RandomAccessFile} access.
|
||||||
|
*/
|
||||||
|
public class RandomAccessFileDataSource implements DataSource {
|
||||||
|
|
||||||
|
private static final int MAX_READ_CHUNK_SIZE = 1024 * 1024;
|
||||||
|
|
||||||
|
private final FileChannel mChannel;
|
||||||
|
private final long mOffset;
|
||||||
|
private final long mSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
|
||||||
|
* whole file. Changes to the contents of the file, including the size of the file,
|
||||||
|
* will be visible in this data source.
|
||||||
|
*/
|
||||||
|
public RandomAccessFileDataSource(RandomAccessFile file) {
|
||||||
|
mChannel = file.getChannel();
|
||||||
|
mOffset = 0;
|
||||||
|
mSize = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
|
||||||
|
* specified region of the provided file. Changes to the contents of the file will be visible in
|
||||||
|
* this data source.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative.
|
||||||
|
*/
|
||||||
|
public RandomAccessFileDataSource(RandomAccessFile file, long offset, long size) {
|
||||||
|
this(file.getChannel(), offset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RandomAccessFileDataSource(FileChannel channel, long offset, long size) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + size);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
mChannel = channel;
|
||||||
|
mOffset = offset;
|
||||||
|
mSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
if (mSize == -1) {
|
||||||
|
try {
|
||||||
|
return mChannel.size();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return mSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomAccessFileDataSource slice(long offset, long size) {
|
||||||
|
long sourceSize = size();
|
||||||
|
checkChunkValid(offset, size, sourceSize);
|
||||||
|
if ((offset == 0) && (size == sourceSize)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RandomAccessFileDataSource(mChannel, mOffset + offset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||||
|
long sourceSize = size();
|
||||||
|
checkChunkValid(offset, size, sourceSize);
|
||||||
|
if (size == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long chunkOffsetInFile = mOffset + offset;
|
||||||
|
long remaining = size;
|
||||||
|
ByteBuffer buf = ByteBuffer.allocateDirect((int) Math.min(remaining, MAX_READ_CHUNK_SIZE));
|
||||||
|
|
||||||
|
while (remaining > 0) {
|
||||||
|
int chunkSize = (int) Math.min(remaining, buf.capacity());
|
||||||
|
int chunkRemaining = chunkSize;
|
||||||
|
buf.limit(chunkSize);
|
||||||
|
synchronized (mChannel) {
|
||||||
|
mChannel.position(chunkOffsetInFile);
|
||||||
|
while (chunkRemaining > 0) {
|
||||||
|
int read = mChannel.read(buf);
|
||||||
|
if (read < 0) {
|
||||||
|
throw new IOException("Unexpected EOF encountered");
|
||||||
|
}
|
||||||
|
chunkRemaining -= read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.flip();
|
||||||
|
sink.consume(buf);
|
||||||
|
buf.clear();
|
||||||
|
chunkOffsetInFile += chunkSize;
|
||||||
|
remaining -= chunkSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||||
|
long sourceSize = size();
|
||||||
|
checkChunkValid(offset, size, sourceSize);
|
||||||
|
if (size == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (size > dest.remaining()) {
|
||||||
|
throw new BufferOverflowException();
|
||||||
|
}
|
||||||
|
|
||||||
|
long offsetInFile = mOffset + offset;
|
||||||
|
int remaining = size;
|
||||||
|
int prevLimit = dest.limit();
|
||||||
|
try {
|
||||||
|
// FileChannel.read(ByteBuffer) reads up to dest.remaining(). Thus, we need to adjust
|
||||||
|
// the buffer's limit to avoid reading more than size bytes.
|
||||||
|
dest.limit(dest.position() + size);
|
||||||
|
while (remaining > 0) {
|
||||||
|
int chunkSize;
|
||||||
|
synchronized (mChannel) {
|
||||||
|
mChannel.position(offsetInFile);
|
||||||
|
chunkSize = mChannel.read(dest);
|
||||||
|
}
|
||||||
|
offsetInFile += chunkSize;
|
||||||
|
remaining -= chunkSize;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
dest.limit(prevLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(size);
|
||||||
|
copyTo(offset, size, result);
|
||||||
|
result.flip();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkChunkValid(long offset, long size, long sourceSize) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
if (offset > sourceSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") > source size (" + sourceSize + ")");
|
||||||
|
}
|
||||||
|
long endOffset = offset + size;
|
||||||
|
if (endOffset < offset) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||||
|
}
|
||||||
|
if (endOffset > sourceSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size
|
||||||
|
+ ") > source size (" + sourceSize +")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,216 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.zip.ZipUtils;
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.DataSources;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VerityTreeBuilder is used to generate the root hash of verity tree built from the input file.
|
||||||
|
* The root hash can be used on device for on-access verification. The tree itself is reproducible
|
||||||
|
* on device, and is not shipped with the APK.
|
||||||
|
*/
|
||||||
|
public class VerityTreeBuilder {
|
||||||
|
|
||||||
|
/** Maximum size (in bytes) of each node of the tree. */
|
||||||
|
private final static int CHUNK_SIZE = 4096;
|
||||||
|
|
||||||
|
/** Digest algorithm (JCA Digest algorithm name) used in the tree. */
|
||||||
|
private final static String JCA_ALGORITHM = "SHA-256";
|
||||||
|
|
||||||
|
/** Optional salt to apply before each digestion. */
|
||||||
|
private final byte[] mSalt;
|
||||||
|
|
||||||
|
private final MessageDigest mMd;
|
||||||
|
|
||||||
|
public VerityTreeBuilder(byte[] salt) throws NoSuchAlgorithmException {
|
||||||
|
mSalt = salt;
|
||||||
|
mMd = MessageDigest.getInstance(JCA_ALGORITHM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the root hash of the APK verity tree built from ZIP blocks.
|
||||||
|
*
|
||||||
|
* Specifically, APK verity tree is built from the APK, but as if the APK Signing Block (which
|
||||||
|
* must be page aligned) and the "Central Directory offset" field in End of Central Directory
|
||||||
|
* are skipped.
|
||||||
|
*/
|
||||||
|
public byte[] generateVerityTreeRootHash(DataSource beforeApkSigningBlock,
|
||||||
|
DataSource centralDir, DataSource eocd) throws IOException {
|
||||||
|
if (beforeApkSigningBlock.size() % CHUNK_SIZE != 0) {
|
||||||
|
throw new IllegalStateException("APK Signing Block size not a multiple of " + CHUNK_SIZE
|
||||||
|
+ ": " + beforeApkSigningBlock.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that, when digesting, ZIP End of Central Directory record's Central Directory
|
||||||
|
// offset field is treated as pointing to the offset at which the APK Signing Block will
|
||||||
|
// start.
|
||||||
|
long centralDirOffsetForDigesting = beforeApkSigningBlock.size();
|
||||||
|
ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size());
|
||||||
|
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
eocd.copyTo(0, (int) eocd.size(), eocdBuf);
|
||||||
|
eocdBuf.flip();
|
||||||
|
ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting);
|
||||||
|
|
||||||
|
return generateVerityTreeRootHash(new ChainedDataSource(beforeApkSigningBlock, centralDir,
|
||||||
|
DataSources.asDataSource(eocdBuf)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the root hash of the verity tree built from the data source.
|
||||||
|
*
|
||||||
|
* The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the
|
||||||
|
* input file. If the total size is larger than 4 KB, take this level as input and repeat the
|
||||||
|
* same procedure, until the level is within 4 KB. If salt is given, it will apply to each
|
||||||
|
* digestion before the actual data.
|
||||||
|
*
|
||||||
|
* The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt.
|
||||||
|
*
|
||||||
|
* The tree is currently stored only in memory and is never written out. Nevertheless, it is
|
||||||
|
* the actual verity tree format on disk, and is supposed to be re-generated on device.
|
||||||
|
*
|
||||||
|
* This is package-private for testing purpose.
|
||||||
|
*/
|
||||||
|
byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
|
||||||
|
int digestSize = mMd.getDigestLength();
|
||||||
|
|
||||||
|
// Calculate the summed area table of level size. In other word, this is the offset
|
||||||
|
// table of each level, plus the next non-existing level.
|
||||||
|
int[] levelOffset = calculateLevelOffset(fileSource.size(), digestSize);
|
||||||
|
|
||||||
|
ByteBuffer verityBuffer = ByteBuffer.allocate(levelOffset[levelOffset.length - 1]);
|
||||||
|
|
||||||
|
// Generate the hash tree bottom-up.
|
||||||
|
for (int i = levelOffset.length - 2; i >= 0; i--) {
|
||||||
|
DataSink middleBufferSink = new ByteBufferSink(
|
||||||
|
slice(verityBuffer, levelOffset[i], levelOffset[i + 1]));
|
||||||
|
DataSource src;
|
||||||
|
if (i == levelOffset.length - 2) {
|
||||||
|
src = fileSource;
|
||||||
|
digestDataByChunks(src, middleBufferSink);
|
||||||
|
} else {
|
||||||
|
src = DataSources.asDataSource(slice(verityBuffer.asReadOnlyBuffer(),
|
||||||
|
levelOffset[i + 1], levelOffset[i + 2]));
|
||||||
|
digestDataByChunks(src, middleBufferSink);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the output is not full chunk, pad with 0s.
|
||||||
|
long totalOutput = divideRoundup(src.size(), CHUNK_SIZE) * digestSize;
|
||||||
|
int incomplete = (int) (totalOutput % CHUNK_SIZE);
|
||||||
|
if (incomplete > 0) {
|
||||||
|
byte[] padding = new byte[CHUNK_SIZE - incomplete];
|
||||||
|
middleBufferSink.consume(padding, 0, padding.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, calculate the root hash from the top level (only page).
|
||||||
|
ByteBuffer firstPage = slice(verityBuffer.asReadOnlyBuffer(), 0, CHUNK_SIZE);
|
||||||
|
return saltedDigest(firstPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of summed area table of level size in the verity tree. In other words, the
|
||||||
|
* returned array is offset of each level in the verity tree file format, plus an additional
|
||||||
|
* offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size
|
||||||
|
* is level + 1.
|
||||||
|
*/
|
||||||
|
private static int[] calculateLevelOffset(long dataSize, int digestSize) {
|
||||||
|
// Compute total size of each level, bottom to top.
|
||||||
|
ArrayList<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 {
|
||||||
|
long size = dataSource.size();
|
||||||
|
long offset = 0;
|
||||||
|
for (; offset + CHUNK_SIZE <= size; offset += CHUNK_SIZE) {
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(CHUNK_SIZE);
|
||||||
|
dataSource.copyTo(offset, CHUNK_SIZE, buffer);
|
||||||
|
buffer.rewind();
|
||||||
|
byte[] hash = saltedDigest(buffer);
|
||||||
|
dataSink.consume(hash, 0, hash.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the last incomplete chunk with 0 padding to the sink at once.
|
||||||
|
int remaining = (int) (size % CHUNK_SIZE);
|
||||||
|
if (remaining > 0) {
|
||||||
|
ByteBuffer buffer;
|
||||||
|
buffer = ByteBuffer.allocate(CHUNK_SIZE); // initialized to 0.
|
||||||
|
dataSource.copyTo(offset, remaining, buffer);
|
||||||
|
buffer.rewind();
|
||||||
|
|
||||||
|
byte[] hash = saltedDigest(buffer);
|
||||||
|
dataSink.consume(hash, 0, hash.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the digest of data with salt prepanded. */
|
||||||
|
private byte[] saltedDigest(ByteBuffer data) {
|
||||||
|
mMd.reset();
|
||||||
|
if (mSalt != null) {
|
||||||
|
mMd.update(mSalt);
|
||||||
|
}
|
||||||
|
mMd.update(data);
|
||||||
|
return mMd.digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Divides a number and round up to the closest integer. */
|
||||||
|
private static long divideRoundup(long dividend, long divisor) {
|
||||||
|
return (dividend + divisor - 1) / divisor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a slice of the buffer with shared the content. */
|
||||||
|
private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) {
|
||||||
|
ByteBuffer b = buffer.duplicate();
|
||||||
|
b.position(0); // to ensure position <= limit invariant.
|
||||||
|
b.limit(end);
|
||||||
|
b.position(begin);
|
||||||
|
return b.slice();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,278 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1BerParser;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1DecodingException;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1DerEncoder;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1EncodingException;
|
||||||
|
import com.android.apksig.internal.x509.Certificate;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods to generate {@code X509Certificate}s from their encoded form. These methods
|
||||||
|
* can be used to generate certificates that would be rejected by the Java {@code
|
||||||
|
* CertificateFactory}.
|
||||||
|
*/
|
||||||
|
public class X509CertificateUtils {
|
||||||
|
|
||||||
|
private static CertificateFactory sCertFactory = null;
|
||||||
|
|
||||||
|
// The PEM certificate header and footer as specified in RFC 7468:
|
||||||
|
// There is exactly one space character (SP) separating the "BEGIN" or
|
||||||
|
// "END" from the label. There are exactly five hyphen-minus (also
|
||||||
|
// known as dash) characters ("-") on both ends of the encapsulation
|
||||||
|
// boundaries, no more, no less.
|
||||||
|
public static final byte[] BEGIN_CERT_HEADER = "-----BEGIN CERTIFICATE-----".getBytes();
|
||||||
|
public static final byte[] END_CERT_FOOTER = "-----END CERTIFICATE-----".getBytes();
|
||||||
|
|
||||||
|
private static void buildCertFactory() {
|
||||||
|
if (sCertFactory != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sCertFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new RuntimeException("Failed to create X.509 CertificateFactory", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an {@code X509Certificate} from the {@code InputStream}.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the {@code InputStream} cannot be decoded to a valid
|
||||||
|
* certificate.
|
||||||
|
*/
|
||||||
|
public static X509Certificate generateCertificate(InputStream in) throws CertificateException {
|
||||||
|
byte[] encodedForm;
|
||||||
|
try {
|
||||||
|
encodedForm = ByteStreams.toByteArray(in);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CertificateException("Failed to parse certificate", e);
|
||||||
|
}
|
||||||
|
return generateCertificate(encodedForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an {@code X509Certificate} from the encoded form.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
|
||||||
|
*/
|
||||||
|
public static X509Certificate generateCertificate(byte[] encodedForm)
|
||||||
|
throws CertificateException {
|
||||||
|
if (sCertFactory == null) {
|
||||||
|
buildCertFactory();
|
||||||
|
}
|
||||||
|
return generateCertificate(encodedForm, sCertFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an {@code X509Certificate} from the encoded form using the provided
|
||||||
|
* {@code CertificateFactory}.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
|
||||||
|
*/
|
||||||
|
public static X509Certificate generateCertificate(byte[] encodedForm,
|
||||||
|
CertificateFactory certFactory) throws CertificateException {
|
||||||
|
X509Certificate certificate;
|
||||||
|
try {
|
||||||
|
certificate = (X509Certificate) certFactory.generateCertificate(
|
||||||
|
new ByteArrayInputStream(encodedForm));
|
||||||
|
return certificate;
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
// This could be expected if the certificate is encoded using a BER encoding that does
|
||||||
|
// not use the minimum number of bytes to represent the length of the contents; attempt
|
||||||
|
// to decode the certificate using the BER parser and re-encode using the DER encoder
|
||||||
|
// below.
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Some apps were previously signed with a BER encoded certificate that now results
|
||||||
|
// in exceptions from the CertificateFactory generateCertificate(s) methods. Since
|
||||||
|
// the original BER encoding of the certificate is used as the signature for these
|
||||||
|
// apps that original encoding must be maintained when signing updated versions of
|
||||||
|
// these apps and any new apps that may require capabilities guarded by the
|
||||||
|
// signature. To maintain the same signature the BER parser can be used to parse
|
||||||
|
// the certificate, then it can be re-encoded to its DER equivalent which is
|
||||||
|
// accepted by the generateCertificate method. The positions in the ByteBuffer can
|
||||||
|
// then be used with the GuaranteedEncodedFormX509Certificate object to ensure the
|
||||||
|
// getEncoded method returns the original signature of the app.
|
||||||
|
ByteBuffer encodedCertBuffer = getNextDEREncodedCertificateBlock(
|
||||||
|
ByteBuffer.wrap(encodedForm));
|
||||||
|
int startingPos = encodedCertBuffer.position();
|
||||||
|
Certificate reencodedCert = Asn1BerParser.parse(encodedCertBuffer, Certificate.class);
|
||||||
|
byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
|
||||||
|
certificate = (X509Certificate) certFactory.generateCertificate(
|
||||||
|
new ByteArrayInputStream(reencodedForm));
|
||||||
|
// If the reencodedForm is successfully accepted by the CertificateFactory then copy the
|
||||||
|
// original encoding from the ByteBuffer and use that encoding in the Guaranteed object.
|
||||||
|
byte[] originalEncoding = new byte[encodedCertBuffer.position() - startingPos];
|
||||||
|
encodedCertBuffer.position(startingPos);
|
||||||
|
encodedCertBuffer.get(originalEncoding);
|
||||||
|
GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
|
||||||
|
new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
|
||||||
|
return guaranteedEncodedCert;
|
||||||
|
} catch (Asn1DecodingException | Asn1EncodingException | CertificateException e) {
|
||||||
|
throw new CertificateException("Failed to parse certificate", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
|
||||||
|
* InputStream}.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the InputStream cannot be decoded to zero or more valid
|
||||||
|
* {@code Certificate} objects.
|
||||||
|
*/
|
||||||
|
public static Collection<? extends java.security.cert.Certificate> generateCertificates(
|
||||||
|
InputStream in) throws CertificateException {
|
||||||
|
if (sCertFactory == null) {
|
||||||
|
buildCertFactory();
|
||||||
|
}
|
||||||
|
return generateCertificates(in, sCertFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
|
||||||
|
* InputStream} using the provided {@code CertificateFactory}.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the InputStream cannot be decoded to zero or more valid
|
||||||
|
* {@code Certificates} objects.
|
||||||
|
*/
|
||||||
|
public static Collection<? 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.x509;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X509 {@code Certificate} as specified in RFC 5280.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class Certificate {
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.SEQUENCE)
|
||||||
|
public TBSCertificate certificate;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.SEQUENCE)
|
||||||
|
public AlgorithmIdentifier signatureAlgorithm;
|
||||||
|
|
||||||
|
@Asn1Field(index = 2, type = Asn1Type.BIT_STRING)
|
||||||
|
public ByteBuffer signature;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.zip;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZIP End of Central Directory record.
|
||||||
|
*/
|
||||||
|
public class EocdRecord {
|
||||||
|
private static final int CD_RECORD_COUNT_ON_DISK_OFFSET = 8;
|
||||||
|
private static final int CD_RECORD_COUNT_TOTAL_OFFSET = 10;
|
||||||
|
private static final int CD_SIZE_OFFSET = 12;
|
||||||
|
private static final int CD_OFFSET_OFFSET = 16;
|
||||||
|
|
||||||
|
public static ByteBuffer createWithModifiedCentralDirectoryInfo(
|
||||||
|
ByteBuffer original,
|
||||||
|
int centralDirectoryRecordCount,
|
||||||
|
long centralDirectorySizeBytes,
|
||||||
|
long centralDirectoryOffset) {
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(original.remaining());
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.put(original.slice());
|
||||||
|
result.flip();
|
||||||
|
ZipUtils.setUnsignedInt16(
|
||||||
|
result, CD_RECORD_COUNT_ON_DISK_OFFSET, centralDirectoryRecordCount);
|
||||||
|
ZipUtils.setUnsignedInt16(
|
||||||
|
result, CD_RECORD_COUNT_TOTAL_OFFSET, centralDirectoryRecordCount);
|
||||||
|
ZipUtils.setUnsignedInt32(result, CD_SIZE_OFFSET, centralDirectorySizeBytes);
|
||||||
|
ZipUtils.setUnsignedInt32(result, CD_OFFSET_OFFSET, centralDirectoryOffset);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,537 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.zip;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.util.ByteBufferSink;
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.zip.ZipFormatException;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.zip.DataFormatException;
|
||||||
|
import java.util.zip.Inflater;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZIP Local File record.
|
||||||
|
*
|
||||||
|
* <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 = new byte[(int) cdRecord.getUncompressedSize()];
|
||||||
|
ByteBuffer resultBuf = ByteBuffer.wrap(result);
|
||||||
|
ByteBufferSink resultSink = new ByteBufferSink(resultBuf);
|
||||||
|
outputUncompressedData(
|
||||||
|
source,
|
||||||
|
cdRecord,
|
||||||
|
cdStartOffsetInArchive,
|
||||||
|
resultSink);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataSink} which inflates received data and outputs the deflated data into the provided
|
||||||
|
* delegate sink.
|
||||||
|
*/
|
||||||
|
private static class InflateSinkAdapter implements DataSink, Closeable {
|
||||||
|
private final DataSink mDelegate;
|
||||||
|
|
||||||
|
private Inflater mInflater = new Inflater(true);
|
||||||
|
private byte[] mOutputBuffer;
|
||||||
|
private byte[] mInputBuffer;
|
||||||
|
private long mOutputByteCount;
|
||||||
|
private boolean mClosed;
|
||||||
|
|
||||||
|
private InflateSinkAdapter(DataSink delegate) {
|
||||||
|
mDelegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
mInflater.setInput(buf, offset, length);
|
||||||
|
if (mOutputBuffer == null) {
|
||||||
|
mOutputBuffer = new byte[65536];
|
||||||
|
}
|
||||||
|
while (!mInflater.finished()) {
|
||||||
|
int outputChunkSize;
|
||||||
|
try {
|
||||||
|
outputChunkSize = mInflater.inflate(mOutputBuffer);
|
||||||
|
} catch (DataFormatException e) {
|
||||||
|
throw new IOException("Failed to inflate data", e);
|
||||||
|
}
|
||||||
|
if (outputChunkSize == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mDelegate.consume(mOutputBuffer, 0, outputChunkSize);
|
||||||
|
mOutputByteCount += outputChunkSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) throws IOException {
|
||||||
|
checkNotClosed();
|
||||||
|
if (buf.hasArray()) {
|
||||||
|
consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
|
||||||
|
buf.position(buf.limit());
|
||||||
|
} else {
|
||||||
|
if (mInputBuffer == null) {
|
||||||
|
mInputBuffer = new byte[65536];
|
||||||
|
}
|
||||||
|
while (buf.hasRemaining()) {
|
||||||
|
int chunkSize = Math.min(buf.remaining(), mInputBuffer.length);
|
||||||
|
buf.get(mInputBuffer, 0, chunkSize);
|
||||||
|
consume(mInputBuffer, 0, chunkSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOutputByteCount() {
|
||||||
|
return mOutputByteCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
mClosed = true;
|
||||||
|
mInputBuffer = null;
|
||||||
|
mOutputBuffer = null;
|
||||||
|
if (mInflater != null) {
|
||||||
|
mInflater.end();
|
||||||
|
mInflater = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkNotClosed() {
|
||||||
|
if (mClosed) {
|
||||||
|
throw new IllegalStateException("Closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.internal.zip;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
import java.util.zip.Deflater;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assorted ZIP format helpers.
|
||||||
|
*
|
||||||
|
* <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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.util;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.util.ByteBufferDataSource;
|
||||||
|
import com.android.apksig.internal.util.RandomAccessFileDataSource;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods for working with {@link DataSource} abstraction.
|
||||||
|
*/
|
||||||
|
public abstract class DataSources {
|
||||||
|
private DataSources() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link DataSource} backed by the provided {@link ByteBuffer}. The data source
|
||||||
|
* represents the data contained between the position and limit of the buffer. Changes to the
|
||||||
|
* buffer's contents will be visible in the data source.
|
||||||
|
*/
|
||||||
|
public static DataSource asDataSource(ByteBuffer buffer) {
|
||||||
|
if (buffer == null) {
|
||||||
|
throw new NullPointerException();
|
||||||
|
}
|
||||||
|
return new ByteBufferDataSource(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link DataSource} backed by the provided {@link RandomAccessFile}. Changes to the
|
||||||
|
* file, including changes to size of file, will be visible in the data source.
|
||||||
|
*/
|
||||||
|
public static DataSource asDataSource(RandomAccessFile file) {
|
||||||
|
if (file == null) {
|
||||||
|
throw new NullPointerException();
|
||||||
|
}
|
||||||
|
return new RandomAccessFileDataSource(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link DataSource} backed by the provided region of the {@link RandomAccessFile}.
|
||||||
|
* Changes to the file will be visible in the data source.
|
||||||
|
*/
|
||||||
|
public static DataSource asDataSource(RandomAccessFile file, long offset, long size) {
|
||||||
|
if (file == null) {
|
||||||
|
throw new NullPointerException();
|
||||||
|
}
|
||||||
|
return new RandomAccessFileDataSource(file, offset, size);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.util;
|
||||||
|
|
||||||
|
public interface RunnablesExecutor {
|
||||||
|
RunnablesExecutor SINGLE_THREADED = p -> p.getRunnable().run();
|
||||||
|
|
||||||
|
void execute(RunnablesProvider provider);
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.apksig.util;
|
||||||
|
|
||||||
|
public interface RunnablesProvider {
|
||||||
|
Runnable getRunnable();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue