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"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 20
|
||||
versionName "1.3.5"
|
||||
versionCode 21
|
||||
versionName "1.3.6"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled true
|
||||
|
|
|
@ -138,6 +138,7 @@
|
|||
-keep class com.zane.** { *; }
|
||||
-keep class pxb.android.** { *; }
|
||||
-keep class net.fornwall.apksigner.** { *; }
|
||||
-keep class com.android.apksig.** { *; }
|
||||
-keep class org.spongycastle.**
|
||||
-dontwarn org.spongycastle.jce.provider.X509LDAPCertStoreSpi
|
||||
-dontwarn org.spongycastle.x509.util.LDAPStoreHelper
|
||||
|
|
Binary file not shown.
|
@ -2,6 +2,11 @@
|
|||
"minBuildCode": 138,
|
||||
"maxBuildCode": null,
|
||||
"basePath": "",
|
||||
"targetPackageName": [
|
||||
"com.chucklefish.stardewvalley",
|
||||
"com.zane.stardewvalley",
|
||||
"com.martyrpher.stardewvalley"
|
||||
],
|
||||
"manifestEntries": [
|
||||
{
|
||||
"targetPath": "classes.dex",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": 5,
|
||||
"version": 6,
|
||||
"contents": [
|
||||
{
|
||||
"type": "COMPAT",
|
||||
|
@ -9,6 +9,14 @@
|
|||
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
|
||||
"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",
|
||||
"name": "Chinese Locale v2.5.1",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": 5,
|
||||
"version": 6,
|
||||
"contents": [
|
||||
{
|
||||
"type": "COMPAT",
|
||||
|
@ -9,6 +9,14 @@
|
|||
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
|
||||
"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",
|
||||
"name": "Chinese Locale v2.5.1",
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
{
|
||||
"version": 5,
|
||||
"version": 6,
|
||||
"contents": [
|
||||
{
|
||||
"type": "COMPAT",
|
||||
"name": "SMAPI for 1.4.5.137",
|
||||
"name": "SMAPI兼容包 1.4.5.137",
|
||||
"assetPath": "compat/137/",
|
||||
"description": "SMAPI兼容包, 适用版本1.4.4.128 - 1.4.5.137, SMAPI 3.3.2.0",
|
||||
"url": "http://zaneyork.cn/download/compat/smapi_137.zip",
|
||||
"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",
|
||||
"name": "中文汉化v2.5.1",
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
"com.chucklefish.stardewvalley",
|
||||
"com.chucklefish.stardewvalleysamsung",
|
||||
"com.zane.stardewvalley",
|
||||
"com.zane.stardewvalleysamsung",
|
||||
"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