Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in / Register
Toggle navigation
M
Magisk
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Magisk
Commits
2e95d9f0
Commit
2e95d9f0
authored
Aug 08, 2020
by
vvb2060
Committed by
John Wu
Aug 08, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update to APK Signature Scheme v2
parent
fe238839
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1208 additions
and
536 deletions
+1208
-536
ApkSignerV2.java
...ning/src/main/java/com/topjohnwu/signing/ApkSignerV2.java
+772
-0
SignApk.java
app/signing/src/main/java/com/topjohnwu/signing/SignApk.java
+286
-242
ZipAdjust.java
...igning/src/main/java/com/topjohnwu/signing/ZipAdjust.java
+0
-277
ZipSigner.java
...igning/src/main/java/com/topjohnwu/signing/ZipSigner.java
+8
-9
ZipUtils.java
...signing/src/main/java/com/topjohnwu/signing/ZipUtils.java
+136
-0
Keygen.kt
app/src/main/java/com/topjohnwu/magisk/core/utils/Keygen.kt
+1
-1
PatchAPK.kt
...src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt
+5
-7
No files found.
app/signing/src/main/java/com/topjohnwu/signing/ApkSignerV2.java
0 → 100644
View file @
2e95d9f0
package
com
.
topjohnwu
.
signing
;
import
java.nio.BufferUnderflowException
;
import
java.nio.ByteBuffer
;
import
java.nio.ByteOrder
;
import
java.security.DigestException
;
import
java.security.InvalidAlgorithmParameterException
;
import
java.security.InvalidKeyException
;
import
java.security.KeyFactory
;
import
java.security.MessageDigest
;
import
java.security.NoSuchAlgorithmException
;
import
java.security.PrivateKey
;
import
java.security.PublicKey
;
import
java.security.Signature
;
import
java.security.SignatureException
;
import
java.security.cert.CertificateEncodingException
;
import
java.security.cert.X509Certificate
;
import
java.security.spec.AlgorithmParameterSpec
;
import
java.security.spec.InvalidKeySpecException
;
import
java.security.spec.MGF1ParameterSpec
;
import
java.security.spec.PSSParameterSpec
;
import
java.security.spec.X509EncodedKeySpec
;
import
java.util.ArrayList
;
import
java.util.HashMap
;
import
java.util.HashSet
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Set
;
/**
* 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.
*/
public
abstract
class
ApkSignerV2
{
/*
* The two main goals of APK Signature Scheme v2 are:
* 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
* cover every byte of the APK being signed.
* 2. Enable much faster signature and integrity verification. This is achieved by requiring
* only a minimal amount of APK parsing before the signature is verified, thus completely
* bypassing ZIP entry decompression and by making integrity verification parallelizable by
* employing a hash tree.
*
* The generated signature block is wrapped into an APK Signing Block and inserted into the
* original APK immediately before the start of ZIP Central Directory. This is to ensure that
* JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
* extensibility. For example, a future signature scheme could insert its signatures there as
* well. The contract of the APK Signing Block is that all contents outside of the block must be
* protected by signatures inside the block.
*/
public
static
final
int
SIGNATURE_RSA_PSS_WITH_SHA256
=
0x0101
;
public
static
final
int
SIGNATURE_RSA_PSS_WITH_SHA512
=
0x0102
;
public
static
final
int
SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256
=
0x0103
;
public
static
final
int
SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512
=
0x0104
;
public
static
final
int
SIGNATURE_ECDSA_WITH_SHA256
=
0x0201
;
public
static
final
int
SIGNATURE_ECDSA_WITH_SHA512
=
0x0202
;
public
static
final
int
SIGNATURE_DSA_WITH_SHA256
=
0x0301
;
public
static
final
int
SIGNATURE_DSA_WITH_SHA512
=
0x0302
;
/**
* {@code .SF} file header section attribute indicating that the APK is signed not just with
* JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute
* facilitates v2 signature stripping detection.
*
* <p>The attribute contains a comma-separated set of signature scheme IDs.
*/
public
static
final
String
SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME
=
"X-Android-APK-Signed"
;
public
static
final
String
SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE
=
"2"
;
private
static
final
int
CONTENT_DIGEST_CHUNKED_SHA256
=
0
;
private
static
final
int
CONTENT_DIGEST_CHUNKED_SHA512
=
1
;
private
static
final
int
CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES
=
1024
*
1024
;
private
static
final
byte
[]
APK_SIGNING_BLOCK_MAGIC
=
new
byte
[]
{
0x41
,
0x50
,
0x4b
,
0x20
,
0x53
,
0x69
,
0x67
,
0x20
,
0x42
,
0x6c
,
0x6f
,
0x63
,
0x6b
,
0x20
,
0x34
,
0x32
,
};
private
static
final
int
APK_SIGNATURE_SCHEME_V2_BLOCK_ID
=
0x7109871a
;
private
ApkSignerV2
()
{}
/**
* Signer configuration.
*/
public
static
final
class
SignerConfig
{
/** Private key. */
public
PrivateKey
privateKey
;
/**
* Certificates, with the first certificate containing the public key corresponding to
* {@link #privateKey}.
*/
public
List
<
X509Certificate
>
certificates
;
/**
* List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants).
*/
public
List
<
Integer
>
signatureAlgorithms
;
}
/**
* Signs the provided APK using APK Signature Scheme v2 and returns the signed APK as a list of
* consecutive chunks.
*
* <p>NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections
* of META-INF/*.SF files of APK being signed must contain the
* {@code X-Android-APK-Signed: true} attribute.
*
* @param inputApk contents of the APK to be signed. The APK starts at the current position
* of the buffer and ends at the limit of the buffer.
* @param signerConfigs signer configurations, one for each signer.
*
* @throws ApkParseException if the APK cannot be parsed.
* @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
ByteBuffer
[]
sign
(
ByteBuffer
inputApk
,
List
<
SignerConfig
>
signerConfigs
)
throws
ApkParseException
,
InvalidKeyException
,
SignatureException
{
// Slice/create a view in the inputApk to make sure that:
// 1. inputApk is what's between position and limit of the original inputApk, and
// 2. changes to position, limit, and byte order are not reflected in the original.
ByteBuffer
originalInputApk
=
inputApk
;
inputApk
=
originalInputApk
.
slice
();
inputApk
.
order
(
ByteOrder
.
LITTLE_ENDIAN
);
// Locate ZIP End of Central Directory (EoCD), Central Directory, and check that Central
// Directory is immediately followed by the ZIP End of Central Directory.
int
eocdOffset
=
ZipUtils
.
findZipEndOfCentralDirectoryRecord
(
inputApk
);
if
(
eocdOffset
==
-
1
)
{
throw
new
ApkParseException
(
"Failed to locate ZIP End of Central Directory"
);
}
if
(
ZipUtils
.
isZip64EndOfCentralDirectoryLocatorPresent
(
inputApk
,
eocdOffset
))
{
throw
new
ApkParseException
(
"ZIP64 format not supported"
);
}
inputApk
.
position
(
eocdOffset
);
long
centralDirSizeLong
=
ZipUtils
.
getZipEocdCentralDirectorySizeBytes
(
inputApk
);
if
(
centralDirSizeLong
>
Integer
.
MAX_VALUE
)
{
throw
new
ApkParseException
(
"ZIP Central Directory size out of range: "
+
centralDirSizeLong
);
}
int
centralDirSize
=
(
int
)
centralDirSizeLong
;
long
centralDirOffsetLong
=
ZipUtils
.
getZipEocdCentralDirectoryOffset
(
inputApk
);
if
(
centralDirOffsetLong
>
Integer
.
MAX_VALUE
)
{
throw
new
ApkParseException
(
"ZIP Central Directory offset in file out of range: "
+
centralDirOffsetLong
);
}
int
centralDirOffset
=
(
int
)
centralDirOffsetLong
;
int
expectedEocdOffset
=
centralDirOffset
+
centralDirSize
;
if
(
expectedEocdOffset
<
centralDirOffset
)
{
throw
new
ApkParseException
(
"ZIP Central Directory extent too large. Offset: "
+
centralDirOffset
+
", size: "
+
centralDirSize
);
}
if
(
eocdOffset
!=
expectedEocdOffset
)
{
throw
new
ApkParseException
(
"ZIP Central Directory not immeiately followed by ZIP End of"
+
" Central Directory. CD end: "
+
expectedEocdOffset
+
", EoCD start: "
+
eocdOffset
);
}
// Create ByteBuffers holding the contents of everything before ZIP Central Directory,
// ZIP Central Directory, and ZIP End of Central Directory.
inputApk
.
clear
();
ByteBuffer
beforeCentralDir
=
getByteBuffer
(
inputApk
,
centralDirOffset
);
ByteBuffer
centralDir
=
getByteBuffer
(
inputApk
,
eocdOffset
-
centralDirOffset
);
// Create a copy of End of Central Directory because we'll need modify its contents later.
byte
[]
eocdBytes
=
new
byte
[
inputApk
.
remaining
()];
inputApk
.
get
(
eocdBytes
);
ByteBuffer
eocd
=
ByteBuffer
.
wrap
(
eocdBytes
);
eocd
.
order
(
inputApk
.
order
());
// Figure which which digests to use for APK contents.
Set
<
Integer
>
contentDigestAlgorithms
=
new
HashSet
<>();
for
(
SignerConfig
signerConfig
:
signerConfigs
)
{
for
(
int
signatureAlgorithm
:
signerConfig
.
signatureAlgorithms
)
{
contentDigestAlgorithms
.
add
(
getSignatureAlgorithmContentDigestAlgorithm
(
signatureAlgorithm
));
}
}
// Compute digests of APK contents.
Map
<
Integer
,
byte
[]>
contentDigests
;
// digest algorithm ID -> digest
try
{
contentDigests
=
computeContentDigests
(
contentDigestAlgorithms
,
new
ByteBuffer
[]
{
beforeCentralDir
,
centralDir
,
eocd
});
}
catch
(
DigestException
e
)
{
throw
new
SignatureException
(
"Failed to compute digests of APK"
,
e
);
}
// Sign the digests and wrap the signatures and signer info into an APK Signing Block.
ByteBuffer
apkSigningBlock
=
ByteBuffer
.
wrap
(
generateApkSigningBlock
(
signerConfigs
,
contentDigests
));
// Update Central Directory Offset in End of Central Directory Record. Central Directory
// follows the APK Signing Block and thus is shifted by the size of the APK Signing Block.
centralDirOffset
+=
apkSigningBlock
.
remaining
();
eocd
.
clear
();
ZipUtils
.
setZipEocdCentralDirectoryOffset
(
eocd
,
centralDirOffset
);
// Follow the Java NIO pattern for ByteBuffer whose contents have been consumed.
originalInputApk
.
position
(
originalInputApk
.
limit
());
// Reset positions (to 0) and limits (to capacity) in the ByteBuffers below to follow the
// Java NIO pattern for ByteBuffers which are ready for their contents to be read by caller.
// Contrary to the name, this does not clear the contents of these ByteBuffer.
beforeCentralDir
.
clear
();
centralDir
.
clear
();
eocd
.
clear
();
// Insert APK Signing Block immediately before the ZIP Central Directory.
return
new
ByteBuffer
[]
{
beforeCentralDir
,
apkSigningBlock
,
centralDir
,
eocd
,
};
}
private
static
Map
<
Integer
,
byte
[]>
computeContentDigests
(
Set
<
Integer
>
digestAlgorithms
,
ByteBuffer
[]
contents
)
throws
DigestException
{
// For each digest algorithm the result is computed as follows:
// 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
// The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
// No chunks are produced for empty (zero length) segments.
// 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
// length in bytes (uint32 little-endian) and the chunk's contents.
// 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
// chunks (uint32 little-endian) and the concatenation of digests of chunks of all
// segments in-order.
int
chunkCount
=
0
;
for
(
ByteBuffer
input
:
contents
)
{
chunkCount
+=
getChunkCount
(
input
.
remaining
(),
CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES
);
}
final
Map
<
Integer
,
byte
[]>
digestsOfChunks
=
new
HashMap
<>(
digestAlgorithms
.
size
());
for
(
int
digestAlgorithm
:
digestAlgorithms
)
{
int
digestOutputSizeBytes
=
getContentDigestAlgorithmOutputSizeBytes
(
digestAlgorithm
);
byte
[]
concatenationOfChunkCountAndChunkDigests
=
new
byte
[
5
+
chunkCount
*
digestOutputSizeBytes
];
concatenationOfChunkCountAndChunkDigests
[
0
]
=
0x5a
;
setUnsignedInt32LittleEngian
(
chunkCount
,
concatenationOfChunkCountAndChunkDigests
,
1
);
digestsOfChunks
.
put
(
digestAlgorithm
,
concatenationOfChunkCountAndChunkDigests
);
}
int
chunkIndex
=
0
;
byte
[]
chunkContentPrefix
=
new
byte
[
5
];
chunkContentPrefix
[
0
]
=
(
byte
)
0xa5
;
// Optimization opportunity: digests of chunks can be computed in parallel.
for
(
ByteBuffer
input
:
contents
)
{
while
(
input
.
hasRemaining
())
{
int
chunkSize
=
Math
.
min
(
input
.
remaining
(),
CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES
);
final
ByteBuffer
chunk
=
getByteBuffer
(
input
,
chunkSize
);
for
(
int
digestAlgorithm
:
digestAlgorithms
)
{
String
jcaAlgorithmName
=
getContentDigestAlgorithmJcaDigestAlgorithm
(
digestAlgorithm
);
MessageDigest
md
;
try
{
md
=
MessageDigest
.
getInstance
(
jcaAlgorithmName
);
}
catch
(
NoSuchAlgorithmException
e
)
{
throw
new
DigestException
(
jcaAlgorithmName
+
" MessageDigest not supported"
,
e
);
}
// Reset position to 0 and limit to capacity. Position would've been modified
// by the preceding iteration of this loop. NOTE: Contrary to the method name,
// this does not modify the contents of the chunk.
chunk
.
clear
();
setUnsignedInt32LittleEngian
(
chunk
.
remaining
(),
chunkContentPrefix
,
1
);
md
.
update
(
chunkContentPrefix
);
md
.
update
(
chunk
);
byte
[]
concatenationOfChunkCountAndChunkDigests
=
digestsOfChunks
.
get
(
digestAlgorithm
);
int
expectedDigestSizeBytes
=
getContentDigestAlgorithmOutputSizeBytes
(
digestAlgorithm
);
int
actualDigestSizeBytes
=
md
.
digest
(
concatenationOfChunkCountAndChunkDigests
,
5
+
chunkIndex
*
expectedDigestSizeBytes
,
expectedDigestSizeBytes
);
if
(
actualDigestSizeBytes
!=
expectedDigestSizeBytes
)
{
throw
new
DigestException
(
"Unexpected output size of "
+
md
.
getAlgorithm
()
+
" digest: "
+
actualDigestSizeBytes
);
}
}
chunkIndex
++;
}
}
Map
<
Integer
,
byte
[]>
result
=
new
HashMap
<>(
digestAlgorithms
.
size
());
for
(
Map
.
Entry
<
Integer
,
byte
[]>
entry
:
digestsOfChunks
.
entrySet
())
{
int
digestAlgorithm
=
entry
.
getKey
();
byte
[]
concatenationOfChunkCountAndChunkDigests
=
entry
.
getValue
();
String
jcaAlgorithmName
=
getContentDigestAlgorithmJcaDigestAlgorithm
(
digestAlgorithm
);
MessageDigest
md
;
try
{
md
=
MessageDigest
.
getInstance
(
jcaAlgorithmName
);
}
catch
(
NoSuchAlgorithmException
e
)
{
throw
new
DigestException
(
jcaAlgorithmName
+
" MessageDigest not supported"
,
e
);
}
result
.
put
(
digestAlgorithm
,
md
.
digest
(
concatenationOfChunkCountAndChunkDigests
));
}
return
result
;
}
private
static
int
getChunkCount
(
int
inputSize
,
int
chunkSize
)
{
return
(
inputSize
+
chunkSize
-
1
)
/
chunkSize
;
}
private
static
void
setUnsignedInt32LittleEngian
(
int
value
,
byte
[]
result
,
int
offset
)
{
result
[
offset
]
=
(
byte
)
(
value
&
0xff
);
result
[
offset
+
1
]
=
(
byte
)
((
value
>>
8
)
&
0xff
);
result
[
offset
+
2
]
=
(
byte
)
((
value
>>
16
)
&
0xff
);
result
[
offset
+
3
]
=
(
byte
)
((
value
>>
24
)
&
0xff
);
}
private
static
byte
[]
generateApkSigningBlock
(
List
<
SignerConfig
>
signerConfigs
,
Map
<
Integer
,
byte
[]>
contentDigests
)
throws
InvalidKeyException
,
SignatureException
{
byte
[]
apkSignatureSchemeV2Block
=
generateApkSignatureSchemeV2Block
(
signerConfigs
,
contentDigests
);
return
generateApkSigningBlock
(
apkSignatureSchemeV2Block
);
}
private
static
byte
[]
generateApkSigningBlock
(
byte
[]
apkSignatureSchemeV2Block
)
{
// FORMAT:
// uint64: size (excluding this field)
// repeated ID-value pairs:
// uint64: size (excluding this field)
// uint32: ID
// (size - 4) bytes: value
// uint64: size (same as the one above)
// uint128: magic
int
resultSize
=
8
// size
+
8
+
4
+
apkSignatureSchemeV2Block
.
length
// v2Block as ID-value pair
+
8
// size
+
16
// magic
;
ByteBuffer
result
=
ByteBuffer
.
allocate
(
resultSize
);
result
.
order
(
ByteOrder
.
LITTLE_ENDIAN
);
long
blockSizeFieldValue
=
resultSize
-
8
;
result
.
putLong
(
blockSizeFieldValue
);
long
pairSizeFieldValue
=
4
+
apkSignatureSchemeV2Block
.
length
;
result
.
putLong
(
pairSizeFieldValue
);
result
.
putInt
(
APK_SIGNATURE_SCHEME_V2_BLOCK_ID
);
result
.
put
(
apkSignatureSchemeV2Block
);
result
.
putLong
(
blockSizeFieldValue
);
result
.
put
(
APK_SIGNING_BLOCK_MAGIC
);
return
result
.
array
();
}
private
static
byte
[]
generateApkSignatureSchemeV2Block
(
List
<
SignerConfig
>
signerConfigs
,
Map
<
Integer
,
byte
[]>
contentDigests
)
throws
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
encodeAsSequenceOfLengthPrefixedElements
(
new
byte
[][]
{
encodeAsSequenceOfLengthPrefixedElements
(
signerBlocks
),
});
}
private
static
byte
[]
generateSignerBlock
(
SignerConfig
signerConfig
,
Map
<
Integer
,
byte
[]>
contentDigests
)
throws
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
(
int
signatureAlgorithm
:
signerConfig
.
signatureAlgorithms
)
{
int
contentDigestAlgorithm
=
getSignatureAlgorithmContentDigestAlgorithm
(
signatureAlgorithm
);
byte
[]
contentDigest
=
contentDigests
.
get
(
contentDigestAlgorithm
);
if
(
contentDigest
==
null
)
{
throw
new
RuntimeException
(
getContentDigestAlgorithmJcaDigestAlgorithm
(
contentDigestAlgorithm
)
+
" content digest for "
+
getSignatureAlgorithmJcaSignatureAlgorithm
(
signatureAlgorithm
)
+
" not computed"
);
}
digests
.
add
(
Pair
.
create
(
signatureAlgorithm
,
contentDigest
));
}
signedData
.
digests
=
digests
;
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
),
// additional attributes
new
byte
[
0
],
});
signer
.
publicKey
=
encodedPublicKey
;
signer
.
signatures
=
new
ArrayList
<>();
for
(
int
signatureAlgorithm
:
signerConfig
.
signatureAlgorithms
)
{
Pair
<
String
,
?
extends
AlgorithmParameterSpec
>
signatureParams
=
getSignatureAlgorithmJcaSignatureAlgorithm
(
signatureAlgorithm
);
String
jcaSignatureAlgorithm
=
signatureParams
.
getFirst
();
AlgorithmParameterSpec
jcaSignatureAlgorithmParams
=
signatureParams
.
getSecond
();
byte
[]
signatureBytes
;
try
{
Signature
signature
=
Signature
.
getInstance
(
jcaSignatureAlgorithm
);
signature
.
initSign
(
signerConfig
.
privateKey
);
if
(
jcaSignatureAlgorithmParams
!=
null
)
{
signature
.
setParameter
(
jcaSignatureAlgorithmParams
);
}
signature
.
update
(
signer
.
signedData
);
signatureBytes
=
signature
.
sign
();
}
catch
(
InvalidKeyException
e
)
{
throw
new
InvalidKeyException
(
"Failed sign using "
+
jcaSignatureAlgorithm
,
e
);
}
catch
(
NoSuchAlgorithmException
|
InvalidAlgorithmParameterException
|
SignatureException
e
)
{
throw
new
SignatureException
(
"Failed sign using "
+
jcaSignatureAlgorithm
,
e
);
}
try
{
Signature
signature
=
Signature
.
getInstance
(
jcaSignatureAlgorithm
);
signature
.
initVerify
(
publicKey
);
if
(
jcaSignatureAlgorithmParams
!=
null
)
{
signature
.
setParameter
(
jcaSignatureAlgorithmParams
);
}
signature
.
update
(
signer
.
signedData
);
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
(
NoSuchAlgorithmException
|
InvalidAlgorithmParameterException
|
SignatureException
e
)
{
throw
new
SignatureException
(
"Failed to verify generated "
+
jcaSignatureAlgorithm
+
" signature using public key from certificate"
,
e
);
}
signer
.
signatures
.
add
(
Pair
.
create
(
signatureAlgorithm
,
signatureBytes
));
}
// FORMAT:
// * length-prefixed signed data
// * length-prefixed sequence of length-prefixed signatures:
// * uint32: signature algorithm ID
// * length-prefixed bytes: signature of signed data
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
return
encodeAsSequenceOfLengthPrefixedElements
(
new
byte
[][]
{
signer
.
signedData
,
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes
(
signer
.
signatures
),
signer
.
publicKey
,
});
}
private
static
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
;
}
}
private
static
byte
[]
encodePublicKey
(
PublicKey
publicKey
)
throws
InvalidKeyException
{
byte
[]
encodedPublicKey
=
null
;
if
(
"X.509"
.
equals
(
publicKey
.
getFormat
()))
{
encodedPublicKey
=
publicKey
.
getEncoded
();
}
if
(
encodedPublicKey
==
null
)
{
try
{
encodedPublicKey
=
KeyFactory
.
getInstance
(
publicKey
.
getAlgorithm
())
.
getKeySpec
(
publicKey
,
X509EncodedKeySpec
.
class
)
.
getEncoded
();
}
catch
(
NoSuchAlgorithmException
|
InvalidKeySpecException
e
)
{
throw
new
InvalidKeyException
(
"Failed to obtain X.509 encoded form of public key "
+
publicKey
+
" of class "
+
publicKey
.
getClass
().
getName
(),
e
);
}
}
if
((
encodedPublicKey
==
null
)
||
(
encodedPublicKey
.
length
==
0
))
{
throw
new
InvalidKeyException
(
"Failed to obtain X.509 encoded form of public key "
+
publicKey
+
" of class "
+
publicKey
.
getClass
().
getName
());
}
return
encodedPublicKey
;
}
public
static
List
<
byte
[]>
encodeCertificates
(
List
<
X509Certificate
>
certificates
)
throws
CertificateEncodingException
{
List
<
byte
[]>
result
=
new
ArrayList
<>();
for
(
X509Certificate
certificate
:
certificates
)
{
result
.
add
(
certificate
.
getEncoded
());
}
return
result
;
}
private
static
byte
[]
encodeAsSequenceOfLengthPrefixedElements
(
List
<
byte
[]>
sequence
)
{
return
encodeAsSequenceOfLengthPrefixedElements
(
sequence
.
toArray
(
new
byte
[
sequence
.
size
()][]));
}
private
static
byte
[]
encodeAsSequenceOfLengthPrefixedElements
(
byte
[][]
sequence
)
{
int
payloadSize
=
0
;
for
(
byte
[]
element
:
sequence
)
{
payloadSize
+=
4
+
element
.
length
;
}
ByteBuffer
result
=
ByteBuffer
.
allocate
(
payloadSize
);
result
.
order
(
ByteOrder
.
LITTLE_ENDIAN
);
for
(
byte
[]
element
:
sequence
)
{
result
.
putInt
(
element
.
length
);
result
.
put
(
element
);
}
return
result
.
array
();
}
private
static
byte
[]
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes
(
List
<
Pair
<
Integer
,
byte
[]>>
sequence
)
{
int
resultSize
=
0
;
for
(
Pair
<
Integer
,
byte
[]>
element
:
sequence
)
{
resultSize
+=
12
+
element
.
getSecond
().
length
;
}
ByteBuffer
result
=
ByteBuffer
.
allocate
(
resultSize
);
result
.
order
(
ByteOrder
.
LITTLE_ENDIAN
);
for
(
Pair
<
Integer
,
byte
[]>
element
:
sequence
)
{
byte
[]
second
=
element
.
getSecond
();
result
.
putInt
(
8
+
second
.
length
);
result
.
putInt
(
element
.
getFirst
());
result
.
putInt
(
second
.
length
);
result
.
put
(
second
);
}
return
result
.
array
();
}
/**
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
* position of this buffer.
*
* <p>This method reads the next {@code size} bytes at this buffer's current position,
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
* {@code size}.
*/
private
static
ByteBuffer
getByteBuffer
(
ByteBuffer
source
,
int
size
)
{
if
(
size
<
0
)
{
throw
new
IllegalArgumentException
(
"size: "
+
size
);
}
int
originalLimit
=
source
.
limit
();
int
position
=
source
.
position
();
int
limit
=
position
+
size
;
if
((
limit
<
position
)
||
(
limit
>
originalLimit
))
{
throw
new
BufferUnderflowException
();
}
source
.
limit
(
limit
);
try
{
ByteBuffer
result
=
source
.
slice
();
result
.
order
(
source
.
order
());
source
.
position
(
limit
);
return
result
;
}
finally
{
source
.
limit
(
originalLimit
);
}
}
private
static
Pair
<
String
,
?
extends
AlgorithmParameterSpec
>
getSignatureAlgorithmJcaSignatureAlgorithm
(
int
sigAlgorithm
)
{
switch
(
sigAlgorithm
)
{
case
SIGNATURE_RSA_PSS_WITH_SHA256:
return
Pair
.
create
(
"SHA256withRSA/PSS"
,
new
PSSParameterSpec
(
"SHA-256"
,
"MGF1"
,
MGF1ParameterSpec
.
SHA256
,
256
/
8
,
1
));
case
SIGNATURE_RSA_PSS_WITH_SHA512:
return
Pair
.
create
(
"SHA512withRSA/PSS"
,
new
PSSParameterSpec
(
"SHA-512"
,
"MGF1"
,
MGF1ParameterSpec
.
SHA512
,
512
/
8
,
1
));
case
SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
return
Pair
.
create
(
"SHA256withRSA"
,
null
);
case
SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
return
Pair
.
create
(
"SHA512withRSA"
,
null
);
case
SIGNATURE_ECDSA_WITH_SHA256:
return
Pair
.
create
(
"SHA256withECDSA"
,
null
);
case
SIGNATURE_ECDSA_WITH_SHA512:
return
Pair
.
create
(
"SHA512withECDSA"
,
null
);
case
SIGNATURE_DSA_WITH_SHA256:
return
Pair
.
create
(
"SHA256withDSA"
,
null
);
case
SIGNATURE_DSA_WITH_SHA512:
return
Pair
.
create
(
"SHA512withDSA"
,
null
);
default
:
throw
new
IllegalArgumentException
(
"Unknown signature algorithm: 0x"
+
Long
.
toHexString
(
sigAlgorithm
&
0xffffffff
));
}
}
private
static
int
getSignatureAlgorithmContentDigestAlgorithm
(
int
sigAlgorithm
)
{
switch
(
sigAlgorithm
)
{
case
SIGNATURE_RSA_PSS_WITH_SHA256:
case
SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
case
SIGNATURE_ECDSA_WITH_SHA256:
case
SIGNATURE_DSA_WITH_SHA256:
return
CONTENT_DIGEST_CHUNKED_SHA256
;
case
SIGNATURE_RSA_PSS_WITH_SHA512:
case
SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
case
SIGNATURE_ECDSA_WITH_SHA512:
case
SIGNATURE_DSA_WITH_SHA512:
return
CONTENT_DIGEST_CHUNKED_SHA512
;
default
:
throw
new
IllegalArgumentException
(
"Unknown signature algorithm: 0x"
+
Long
.
toHexString
(
sigAlgorithm
&
0xffffffff
));
}
}
private
static
String
getContentDigestAlgorithmJcaDigestAlgorithm
(
int
digestAlgorithm
)
{
switch
(
digestAlgorithm
)
{
case
CONTENT_DIGEST_CHUNKED_SHA256:
return
"SHA-256"
;
case
CONTENT_DIGEST_CHUNKED_SHA512:
return
"SHA-512"
;
default
:
throw
new
IllegalArgumentException
(
"Unknown content digest algorthm: "
+
digestAlgorithm
);
}
}
private
static
int
getContentDigestAlgorithmOutputSizeBytes
(
int
digestAlgorithm
)
{
switch
(
digestAlgorithm
)
{
case
CONTENT_DIGEST_CHUNKED_SHA256:
return
256
/
8
;
case
CONTENT_DIGEST_CHUNKED_SHA512:
return
512
/
8
;
default
:
throw
new
IllegalArgumentException
(
"Unknown content digest algorthm: "
+
digestAlgorithm
);
}
}
/**
* Indicates that APK file could not be parsed.
*/
public
static
class
ApkParseException
extends
Exception
{
private
static
final
long
serialVersionUID
=
1L
;
public
ApkParseException
(
String
message
)
{
super
(
message
);
}
public
ApkParseException
(
String
message
,
Throwable
cause
)
{
super
(
message
,
cause
);
}
}
/**
* Pair of two elements.
*/
private
static
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
>
create
(
A
first
,
B
second
)
{
return
new
Pair
<>(
first
,
second
);
}
public
A
getFirst
()
{
return
mFirst
;
}
public
B
getSecond
()
{
return
mSecond
;
}
@Override
public
int
hashCode
()
{
final
int
prime
=
31
;
int
result
=
1
;
result
=
prime
*
result
+
((
mFirst
==
null
)
?
0
:
mFirst
.
hashCode
());
result
=
prime
*
result
+
((
mSecond
==
null
)
?
0
:
mSecond
.
hashCode
());
return
result
;
}
@Override
public
boolean
equals
(
Object
obj
)
{
if
(
this
==
obj
)
{
return
true
;
}
if
(
obj
==
null
)
{
return
false
;
}
if
(
getClass
()
!=
obj
.
getClass
())
{
return
false
;
}
@SuppressWarnings
(
"rawtypes"
)
Pair
other
=
(
Pair
)
obj
;
if
(
mFirst
==
null
)
{
if
(
other
.
mFirst
!=
null
)
{
return
false
;
}
}
else
if
(!
mFirst
.
equals
(
other
.
mFirst
))
{
return
false
;
}
if
(
mSecond
==
null
)
{
return
other
.
mSecond
==
null
;
}
else
return
mSecond
.
equals
(
other
.
mSecond
);
}
}
}
app/signing/src/main/java/com/topjohnwu/signing/SignA
PK
.java
→
app/signing/src/main/java/com/topjohnwu/signing/SignA
pk
.java
View file @
2e95d9f0
...
@@ -2,9 +2,7 @@ package com.topjohnwu.signing;
...
@@ -2,9 +2,7 @@ package com.topjohnwu.signing;
import
org.bouncycastle.asn1.ASN1Encoding
;
import
org.bouncycastle.asn1.ASN1Encoding
;
import
org.bouncycastle.asn1.ASN1InputStream
;
import
org.bouncycastle.asn1.ASN1InputStream
;
import
org.bouncycastle.asn1.ASN1ObjectIdentifier
;
import
org.bouncycastle.asn1.ASN1OutputStream
;
import
org.bouncycastle.asn1.ASN1OutputStream
;
import
org.bouncycastle.asn1.cms.CMSObjectIdentifiers
;
import
org.bouncycastle.cert.jcajce.JcaCertStore
;
import
org.bouncycastle.cert.jcajce.JcaCertStore
;
import
org.bouncycastle.cms.CMSException
;
import
org.bouncycastle.cms.CMSException
;
import
org.bouncycastle.cms.CMSProcessableByteArray
;
import
org.bouncycastle.cms.CMSProcessableByteArray
;
...
@@ -12,34 +10,38 @@ import org.bouncycastle.cms.CMSSignedData;
...
@@ -12,34 +10,38 @@ import org.bouncycastle.cms.CMSSignedData;
import
org.bouncycastle.cms.CMSSignedDataGenerator
;
import
org.bouncycastle.cms.CMSSignedDataGenerator
;
import
org.bouncycastle.cms.CMSTypedData
;
import
org.bouncycastle.cms.CMSTypedData
;
import
org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
;
import
org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
;
import
org.bouncycastle.jce.provider.BouncyCastleProvider
;
import
org.bouncycastle.operator.ContentSigner
;
import
org.bouncycastle.operator.ContentSigner
;
import
org.bouncycastle.operator.OperatorCreationException
;
import
org.bouncycastle.operator.OperatorCreationException
;
import
org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
;
import
org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
;
import
org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
;
import
org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
;
import
org.bouncycastle.util.encoders.Base64
;
import
org.bouncycastle.util.encoders.Base64
;
import
java.io.BufferedOutputStream
;
import
java.io.ByteArrayOutputStream
;
import
java.io.ByteArrayOutputStream
;
import
java.io.File
;
import
java.io.FileNotFoundException
;
import
java.io.FileOutputStream
;
import
java.io.FileOutputStream
;
import
java.io.FilterOutputStream
;
import
java.io.FilterOutputStream
;
import
java.io.IOException
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.io.InputStream
;
import
java.io.OutputStream
;
import
java.io.OutputStream
;
import
java.io.PrintStream
;
import
java.io.PrintStream
;
import
java.
io.RandomAccessFile
;
import
java.
nio.ByteBuffer
;
import
java.security.DigestOutputStream
;
import
java.security.DigestOutputStream
;
import
java.security.GeneralSecurityException
;
import
java.security.GeneralSecurityException
;
import
java.security.InvalidKeyException
;
import
java.security.MessageDigest
;
import
java.security.MessageDigest
;
import
java.security.PrivateKey
;
import
java.security.PrivateKey
;
import
java.security.PublicKey
;
import
java.security.Security
;
import
java.security.cert.CertificateEncodingException
;
import
java.security.cert.CertificateEncodingException
;
import
java.security.cert.X509Certificate
;
import
java.security.cert.X509Certificate
;
import
java.util.ArrayList
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.Collections
;
import
java.util.Enumeration
;
import
java.util.Enumeration
;
import
java.util.Iterator
;
import
java.util.List
;
import
java.util.Locale
;
import
java.util.Locale
;
import
java.util.Map
;
import
java.util.Map
;
import
java.util.TimeZone
;
import
java.util.TreeMap
;
import
java.util.TreeMap
;
import
java.util.jar.Attributes
;
import
java.util.jar.Attributes
;
import
java.util.jar.JarEntry
;
import
java.util.jar.JarEntry
;
...
@@ -49,72 +51,28 @@ import java.util.jar.Manifest;
...
@@ -49,72 +51,28 @@ import java.util.jar.Manifest;
import
java.util.regex.Pattern
;
import
java.util.regex.Pattern
;
/*
/*
* Modified from from AOSP
* Modified from from AOSP
* https://android.googlesource.com/platform/build/+/refs/heads/marshmallow-release/tools/signapk/SignApk.java
* https://android.googlesource.com/platform/build/+/refs/tags/android-7.1.2_r39/tools/signapk/src/com/android/signapk/SignApk.java
* */
* */
public
class
SignAPK
{
public
class
SignApk
{
private
static
final
String
CERT_SF_NAME
=
"META-INF/CERT.SF"
;
private
static
final
String
CERT_SF_NAME
=
"META-INF/CERT.SF"
;
private
static
final
String
CERT_SIG_NAME
=
"META-INF/CERT.%s"
;
private
static
final
String
CERT_SIG_NAME
=
"META-INF/CERT.%s"
;
private
static
final
String
CERT_SF_MULTI_NAME
=
"META-INF/CERT%d.SF"
;
private
static
final
String
CERT_SIG_MULTI_NAME
=
"META-INF/CERT%d.%s"
;
// bitmasks for which hash algorithms we need the manifest to include.
// bitmasks for which hash algorithms we need the manifest to include.
private
static
final
int
USE_SHA1
=
1
;
private
static
final
int
USE_SHA1
=
1
;
private
static
final
int
USE_SHA256
=
2
;
private
static
final
int
USE_SHA256
=
2
;
public
static
void
signAndAdjust
(
X509Certificate
cert
,
PrivateKey
key
,
/**
JarMap
input
,
OutputStream
output
)
throws
Exception
{
* Digest algorithm used when signing the APK using APK Signature Scheme v2.
File
temp1
=
File
.
createTempFile
(
"signAPK"
,
null
);
*/
File
temp2
=
File
.
createTempFile
(
"signAPK"
,
null
);
private
static
final
String
APK_SIG_SCHEME_V2_DIGEST_ALGORITHM
=
"SHA-256"
;
// Files matching this pattern are not copied to the output.
try
{
private
static
final
Pattern
stripPattern
=
try
(
OutputStream
out
=
new
BufferedOutputStream
(
new
FileOutputStream
(
temp1
)))
{
Pattern
.
compile
(
"^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
+
sign
(
cert
,
key
,
input
,
out
,
false
);
Pattern
.
quote
(
JarFile
.
MANIFEST_NAME
)
+
")$"
);
}
ZipAdjust
.
adjust
(
temp1
,
temp2
);
try
(
JarMap
map
=
JarMap
.
open
(
temp2
,
false
))
{
sign
(
cert
,
key
,
map
,
output
,
true
);
}
}
finally
{
temp1
.
delete
();
temp2
.
delete
();
}
}
public
static
void
sign
(
X509Certificate
cert
,
PrivateKey
key
,
JarMap
input
,
OutputStream
output
)
throws
Exception
{
sign
(
cert
,
key
,
input
,
output
,
false
);
}
private
static
void
sign
(
X509Certificate
cert
,
PrivateKey
key
,
JarMap
input
,
OutputStream
output
,
boolean
wholeFile
)
throws
Exception
{
int
hashes
=
0
;
hashes
|=
getDigestAlgorithm
(
cert
);
// Set the ZIP file timestamp to the starting valid time
// of the 0th certificate plus one hour (to match what
// we've historically done).
long
timestamp
=
cert
.
getNotBefore
().
getTime
()
+
3600L
*
1000
;
if
(
wholeFile
)
{
signWholeFile
(
input
.
getFile
(),
cert
,
key
,
output
);
}
else
{
JarOutputStream
outputJar
=
new
JarOutputStream
(
output
);
// For signing .apks, use the maximum compression to make
// them as small as possible (since they live forever on
// the system partition). For OTA packages, use the
// default compression level, which is much much faster
// and produces output that is only a tiny bit larger
// (~0.1% on full OTA packages I tested).
outputJar
.
setLevel
(
9
);
Manifest
manifest
=
addDigestsToManifest
(
input
,
hashes
);
copyFiles
(
manifest
,
input
,
outputJar
,
timestamp
,
4
);
signFile
(
manifest
,
input
,
cert
,
key
,
outputJar
);
outputJar
.
close
();
}
}
/**
/**
* Return one of USE_SHA1 or USE_SHA256 according to the signature
* Return one of USE_SHA1 or USE_SHA256 according to the signature
...
@@ -122,7 +80,7 @@ public class SignAPK {
...
@@ -122,7 +80,7 @@ public class SignAPK {
*/
*/
private
static
int
getDigestAlgorithm
(
X509Certificate
cert
)
{
private
static
int
getDigestAlgorithm
(
X509Certificate
cert
)
{
String
sigAlg
=
cert
.
getSigAlgName
().
toUpperCase
(
Locale
.
US
);
String
sigAlg
=
cert
.
getSigAlgName
().
toUpperCase
(
Locale
.
US
);
if
(
sigAlg
.
startsWith
(
"SHA1WITHRSA"
)
||
sigAlg
.
startsWith
(
"MD5WITHRSA"
))
{
if
(
"SHA1WITHRSA"
.
equals
(
sigAlg
)
||
"MD5WITHRSA"
.
equals
(
sigAlg
))
{
return
USE_SHA1
;
return
USE_SHA1
;
}
else
if
(
sigAlg
.
startsWith
(
"SHA256WITH"
))
{
}
else
if
(
sigAlg
.
startsWith
(
"SHA256WITH"
))
{
return
USE_SHA256
;
return
USE_SHA256
;
...
@@ -132,9 +90,10 @@ public class SignAPK {
...
@@ -132,9 +90,10 @@ public class SignAPK {
}
}
}
}
/** Returns the expected signature algorithm for this key type. */
/**
* Returns the expected signature algorithm for this key type.
*/
private
static
String
getSignatureAlgorithm
(
X509Certificate
cert
)
{
private
static
String
getSignatureAlgorithm
(
X509Certificate
cert
)
{
String
sigAlg
=
cert
.
getSigAlgName
().
toUpperCase
(
Locale
.
US
);
String
keyType
=
cert
.
getPublicKey
().
getAlgorithm
().
toUpperCase
(
Locale
.
US
);
String
keyType
=
cert
.
getPublicKey
().
getAlgorithm
().
toUpperCase
(
Locale
.
US
);
if
(
"RSA"
.
equalsIgnoreCase
(
keyType
))
{
if
(
"RSA"
.
equalsIgnoreCase
(
keyType
))
{
if
(
getDigestAlgorithm
(
cert
)
==
USE_SHA256
)
{
if
(
getDigestAlgorithm
(
cert
)
==
USE_SHA256
)
{
...
@@ -149,11 +108,6 @@ public class SignAPK {
...
@@ -149,11 +108,6 @@ public class SignAPK {
}
}
}
}
// Files matching this pattern are not copied to the output.
private
static
Pattern
stripPattern
=
Pattern
.
compile
(
"^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|("
+
Pattern
.
quote
(
JarFile
.
MANIFEST_NAME
)
+
")$"
);
/**
/**
* Add the hash(es) of every file to the manifest, creating it if
* Add the hash(es) of every file to the manifest, creating it if
* necessary.
* necessary.
...
@@ -169,6 +123,7 @@ public class SignAPK {
...
@@ -169,6 +123,7 @@ public class SignAPK {
main
.
putValue
(
"Manifest-Version"
,
"1.0"
);
main
.
putValue
(
"Manifest-Version"
,
"1.0"
);
main
.
putValue
(
"Created-By"
,
"1.0 (Android SignApk)"
);
main
.
putValue
(
"Created-By"
,
"1.0 (Android SignApk)"
);
}
}
MessageDigest
md_sha1
=
null
;
MessageDigest
md_sha1
=
null
;
MessageDigest
md_sha256
=
null
;
MessageDigest
md_sha256
=
null
;
if
((
hashes
&
USE_SHA1
)
!=
0
)
{
if
((
hashes
&
USE_SHA1
)
!=
0
)
{
...
@@ -177,30 +132,51 @@ public class SignAPK {
...
@@ -177,30 +132,51 @@ public class SignAPK {
if
((
hashes
&
USE_SHA256
)
!=
0
)
{
if
((
hashes
&
USE_SHA256
)
!=
0
)
{
md_sha256
=
MessageDigest
.
getInstance
(
"SHA256"
);
md_sha256
=
MessageDigest
.
getInstance
(
"SHA256"
);
}
}
byte
[]
buffer
=
new
byte
[
4096
];
byte
[]
buffer
=
new
byte
[
4096
];
int
num
;
int
num
;
// We sort the input entries by name, and add them to the
// We sort the input entries by name, and add them to the
// output manifest in sorted order. We expect that the output
// output manifest in sorted order. We expect that the output
// map will be deterministic.
// map will be deterministic.
TreeMap
<
String
,
JarEntry
>
byName
=
new
TreeMap
<>();
TreeMap
<
String
,
JarEntry
>
byName
=
new
TreeMap
<>();
for
(
Enumeration
<
JarEntry
>
e
=
jar
.
entries
();
e
.
hasMoreElements
();
)
{
for
(
Enumeration
<
JarEntry
>
e
=
jar
.
entries
();
e
.
hasMoreElements
();
)
{
JarEntry
entry
=
e
.
nextElement
();
JarEntry
entry
=
e
.
nextElement
();
byName
.
put
(
entry
.
getName
(),
entry
);
byName
.
put
(
entry
.
getName
(),
entry
);
}
}
for
(
JarEntry
entry:
byName
.
values
())
{
for
(
JarEntry
entry
:
byName
.
values
())
{
String
name
=
entry
.
getName
();
String
name
=
entry
.
getName
();
if
(!
entry
.
isDirectory
()
&&
if
(!
entry
.
isDirectory
()
&&
!
stripPattern
.
matcher
(
name
).
matches
())
{
(
stripPattern
==
null
||
!
stripPattern
.
matcher
(
name
).
matches
()))
{
InputStream
data
=
jar
.
getInputStream
(
entry
);
InputStream
data
=
jar
.
getInputStream
(
entry
);
while
((
num
=
data
.
read
(
buffer
))
>
0
)
{
while
((
num
=
data
.
read
(
buffer
))
>
0
)
{
if
(
md_sha1
!=
null
)
md_sha1
.
update
(
buffer
,
0
,
num
);
if
(
md_sha1
!=
null
)
md_sha1
.
update
(
buffer
,
0
,
num
);
if
(
md_sha256
!=
null
)
md_sha256
.
update
(
buffer
,
0
,
num
);
if
(
md_sha256
!=
null
)
md_sha256
.
update
(
buffer
,
0
,
num
);
}
}
Attributes
attr
=
new
Attributes
();
Attributes
attr
=
null
;
if
(
input
!=
null
)
attr
=
input
.
getAttributes
(
name
);
attr
=
attr
!=
null
?
new
Attributes
(
attr
)
:
new
Attributes
();
// Remove any previously computed digests from this entry's attributes.
for
(
Iterator
<
Object
>
i
=
attr
.
keySet
().
iterator
();
i
.
hasNext
();
)
{
Object
key
=
i
.
next
();
if
(!(
key
instanceof
Attributes
.
Name
))
{
continue
;
}
String
attributeNameLowerCase
=
key
.
toString
().
toLowerCase
(
Locale
.
US
);
if
(
attributeNameLowerCase
.
endsWith
(
"-digest"
))
{
i
.
remove
();
}
}
// Add SHA-1 digest if requested
if
(
md_sha1
!=
null
)
{
if
(
md_sha1
!=
null
)
{
attr
.
putValue
(
"SHA1-Digest"
,
attr
.
putValue
(
"SHA1-Digest"
,
new
String
(
Base64
.
encode
(
md_sha1
.
digest
()),
"ASCII"
));
new
String
(
Base64
.
encode
(
md_sha1
.
digest
()),
"ASCII"
));
}
}
// Add SHA-256 digest if requested
if
(
md_sha256
!=
null
)
{
if
(
md_sha256
!=
null
)
{
attr
.
putValue
(
"SHA-256-Digest"
,
attr
.
putValue
(
"SHA-256-Digest"
,
new
String
(
Base64
.
encode
(
md_sha256
.
digest
()),
"ASCII"
));
new
String
(
Base64
.
encode
(
md_sha256
.
digest
()),
"ASCII"
));
...
@@ -208,62 +184,39 @@ public class SignAPK {
...
@@ -208,62 +184,39 @@ public class SignAPK {
output
.
getEntries
().
put
(
name
,
attr
);
output
.
getEntries
().
put
(
name
,
attr
);
}
}
}
}
return
output
;
}
/**
return
output
;
* Write to another stream and track how many bytes have been
* written.
*/
private
static
class
CountOutputStream
extends
FilterOutputStream
{
private
int
mCount
;
public
CountOutputStream
(
OutputStream
out
)
{
super
(
out
);
mCount
=
0
;
}
@Override
public
void
write
(
int
b
)
throws
IOException
{
super
.
write
(
b
);
mCount
++;
}
@Override
public
void
write
(
byte
[]
b
,
int
off
,
int
len
)
throws
IOException
{
super
.
write
(
b
,
off
,
len
);
mCount
+=
len
;
}
public
int
size
()
{
return
mCount
;
}
}
}
/**
/**
*
An OutputStream that does literally nothing
*
Write a .SF file with a digest of the specified manifest.
*/
*/
private
static
OutputStream
stubStream
=
new
OutputStream
()
{
private
static
void
writeSignatureFile
(
Manifest
manifest
,
OutputStream
out
,
@Override
int
hash
)
public
void
write
(
int
b
)
{}
@Override
public
void
write
(
byte
[]
b
,
int
off
,
int
len
)
{}
};
/** Write a .SF file with a digest of the specified manifest. */
private
static
void
writeSignatureFile
(
Manifest
manifest
,
OutputStream
out
,
int
hash
)
throws
IOException
,
GeneralSecurityException
{
throws
IOException
,
GeneralSecurityException
{
Manifest
sf
=
new
Manifest
();
Manifest
sf
=
new
Manifest
();
Attributes
main
=
sf
.
getMainAttributes
();
Attributes
main
=
sf
.
getMainAttributes
();
main
.
putValue
(
"Signature-Version"
,
"1.0"
);
main
.
putValue
(
"Signature-Version"
,
"1.0"
);
main
.
putValue
(
"Created-By"
,
"1.0 (Android SignApk)"
);
main
.
putValue
(
"Created-By"
,
"1.0 (Android SignApk)"
);
MessageDigest
md
=
MessageDigest
.
getInstance
(
// Add APK Signature Scheme v2 signature stripping protection.
hash
==
USE_SHA256
?
"SHA256"
:
"SHA1"
);
// This attribute indicates that this APK is supposed to have been signed using one or
PrintStream
print
=
new
PrintStream
(
// more APK-specific signature schemes in addition to the standard JAR signature scheme
new
DigestOutputStream
(
stubStream
,
md
),
// 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.
main
.
putValue
(
ApkSignerV2
.
SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME
,
ApkSignerV2
.
SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE
);
MessageDigest
md
=
MessageDigest
.
getInstance
(
hash
==
USE_SHA256
?
"SHA256"
:
"SHA1"
);
PrintStream
print
=
new
PrintStream
(
new
DigestOutputStream
(
new
ByteArrayOutputStream
(),
md
),
true
,
"UTF-8"
);
true
,
"UTF-8"
);
// Digest of the entire manifest
// Digest of the entire manifest
manifest
.
write
(
print
);
manifest
.
write
(
print
);
print
.
flush
();
print
.
flush
();
main
.
putValue
(
hash
==
USE_SHA256
?
"SHA-256-Digest-Manifest"
:
"SHA1-Digest-Manifest"
,
main
.
putValue
(
hash
==
USE_SHA256
?
"SHA-256-Digest-Manifest"
:
"SHA1-Digest-Manifest"
,
new
String
(
Base64
.
encode
(
md
.
digest
()),
"ASCII"
));
new
String
(
Base64
.
encode
(
md
.
digest
()),
"ASCII"
));
Map
<
String
,
Attributes
>
entries
=
manifest
.
getEntries
();
Map
<
String
,
Attributes
>
entries
=
manifest
.
getEntries
();
for
(
Map
.
Entry
<
String
,
Attributes
>
entry
:
entries
.
entrySet
())
{
for
(
Map
.
Entry
<
String
,
Attributes
>
entry
:
entries
.
entrySet
())
{
// Digest of the manifest stanza for this entry.
// Digest of the manifest stanza for this entry.
...
@@ -273,13 +226,16 @@ public class SignAPK {
...
@@ -273,13 +226,16 @@ public class SignAPK {
}
}
print
.
print
(
"\r\n"
);
print
.
print
(
"\r\n"
);
print
.
flush
();
print
.
flush
();
Attributes
sfAttr
=
new
Attributes
();
Attributes
sfAttr
=
new
Attributes
();
sfAttr
.
putValue
(
hash
==
USE_SHA256
?
"SHA-256-Digest"
:
"SHA1-Digest"
,
sfAttr
.
putValue
(
hash
==
USE_SHA256
?
"SHA-256-Digest"
:
"SHA1-Digest"
,
new
String
(
Base64
.
encode
(
md
.
digest
()),
"ASCII"
));
new
String
(
Base64
.
encode
(
md
.
digest
()),
"ASCII"
));
sf
.
getEntries
().
put
(
entry
.
getKey
(),
sfAttr
);
sf
.
getEntries
().
put
(
entry
.
getKey
(),
sfAttr
);
}
}
CountOutputStream
cout
=
new
CountOutputStream
(
out
);
CountOutputStream
cout
=
new
CountOutputStream
(
out
);
sf
.
write
(
cout
);
sf
.
write
(
cout
);
// A bug in the java.util.jar implementation of Android platforms
// A bug in the java.util.jar implementation of Android platforms
// up to version 1.6 will cause a spurious IOException to be thrown
// 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.
// if the length of the signature file is a multiple of 1024 bytes.
...
@@ -290,10 +246,11 @@ public class SignAPK {
...
@@ -290,10 +246,11 @@ public class SignAPK {
}
}
}
}
/** Sign data and write the digital signature to 'out'. */
/**
* Sign data and write the digital signature to 'out'.
*/
private
static
void
writeSignatureBlock
(
private
static
void
writeSignatureBlock
(
CMSTypedData
data
,
X509Certificate
publicKey
,
PrivateKey
privateKey
,
CMSTypedData
data
,
X509Certificate
publicKey
,
PrivateKey
privateKey
,
OutputStream
out
)
OutputStream
out
)
throws
IOException
,
throws
IOException
,
CertificateEncodingException
,
CertificateEncodingException
,
OperatorCreationException
,
OperatorCreationException
,
...
@@ -301,19 +258,22 @@ public class SignAPK {
...
@@ -301,19 +258,22 @@ public class SignAPK {
ArrayList
<
X509Certificate
>
certList
=
new
ArrayList
<>(
1
);
ArrayList
<
X509Certificate
>
certList
=
new
ArrayList
<>(
1
);
certList
.
add
(
publicKey
);
certList
.
add
(
publicKey
);
JcaCertStore
certs
=
new
JcaCertStore
(
certList
);
JcaCertStore
certs
=
new
JcaCertStore
(
certList
);
CMSSignedDataGenerator
gen
=
new
CMSSignedDataGenerator
();
CMSSignedDataGenerator
gen
=
new
CMSSignedDataGenerator
();
ContentSigner
signer
=
new
JcaContentSignerBuilder
(
getSignatureAlgorithm
(
publicKey
))
ContentSigner
signer
=
new
JcaContentSignerBuilder
(
getSignatureAlgorithm
(
publicKey
))
.
build
(
privateKey
);
.
build
(
privateKey
);
gen
.
addSignerInfoGenerator
(
gen
.
addSignerInfoGenerator
(
new
JcaSignerInfoGeneratorBuilder
(
new
JcaSignerInfoGeneratorBuilder
(
new
JcaDigestCalculatorProviderBuilder
().
build
())
new
JcaDigestCalculatorProviderBuilder
().
build
())
.
setDirectSignature
(
true
)
.
setDirectSignature
(
true
)
.
build
(
signer
,
publicKey
));
.
build
(
signer
,
publicKey
)
);
gen
.
addCertificates
(
certs
);
gen
.
addCertificates
(
certs
);
CMSSignedData
sigData
=
gen
.
generate
(
data
,
false
);
CMSSignedData
sigData
=
gen
.
generate
(
data
,
false
);
ASN1InputStream
asn1
=
new
ASN1InputStream
(
sigData
.
getEncoded
());
ASN1OutputStream
dos
=
ASN1OutputStream
.
create
(
out
,
ASN1Encoding
.
DER
);
try
(
ASN1InputStream
asn1
=
new
ASN1InputStream
(
sigData
.
getEncoded
()))
{
dos
.
writeObject
(
asn1
.
readObject
());
ASN1OutputStream
dos
=
ASN1OutputStream
.
create
(
out
,
ASN1Encoding
.
DER
);
dos
.
writeObject
(
asn1
.
readObject
());
}
}
}
/**
/**
...
@@ -323,27 +283,37 @@ public class SignAPK {
...
@@ -323,27 +283,37 @@ public class SignAPK {
* more efficient.
* more efficient.
*/
*/
private
static
void
copyFiles
(
Manifest
manifest
,
JarMap
in
,
JarOutputStream
out
,
private
static
void
copyFiles
(
Manifest
manifest
,
JarMap
in
,
JarOutputStream
out
,
long
timestamp
,
int
a
lignment
)
throws
IOException
{
long
timestamp
,
int
defaultA
lignment
)
throws
IOException
{
byte
[]
buffer
=
new
byte
[
4096
];
byte
[]
buffer
=
new
byte
[
4096
];
int
num
;
int
num
;
Map
<
String
,
Attributes
>
entries
=
manifest
.
getEntries
();
Map
<
String
,
Attributes
>
entries
=
manifest
.
getEntries
();
ArrayList
<
String
>
names
=
new
ArrayList
<>(
entries
.
keySet
());
ArrayList
<
String
>
names
=
new
ArrayList
<>(
entries
.
keySet
());
Collections
.
sort
(
names
);
Collections
.
sort
(
names
);
boolean
firstEntry
=
true
;
boolean
firstEntry
=
true
;
long
offset
=
0L
;
long
offset
=
0L
;
// We do the copy in two passes -- first copying all the
// We do the copy in two passes -- first copying all the
// entries that are STORED, then copying all the entries that
// entries that are STORED, then copying all the entries that
// have any other compression flag (which in practice means
// have any other compression flag (which in practice means
// DEFLATED). This groups all the stored entries together at
// DEFLATED). This groups all the stored entries together at
// the start of the file and makes it easier to do alignment
// the start of the file and makes it easier to do alignment
// on them (since only stored entries are aligned).
// on them (since only stored entries are aligned).
for
(
String
name
:
names
)
{
for
(
String
name
:
names
)
{
JarEntry
inEntry
=
in
.
getJarEntry
(
name
);
JarEntry
inEntry
=
in
.
getJarEntry
(
name
);
JarEntry
outEntry
=
null
;
JarEntry
outEntry
;
if
(
inEntry
.
getMethod
()
!=
JarEntry
.
STORED
)
continue
;
if
(
inEntry
.
getMethod
()
!=
JarEntry
.
STORED
)
continue
;
// Preserve the STORED method of the input entry.
// Preserve the STORED method of the input entry.
outEntry
=
new
JarEntry
(
inEntry
);
outEntry
=
new
JarEntry
(
inEntry
);
outEntry
.
setTime
(
timestamp
);
outEntry
.
setTime
(
timestamp
);
// Discard comment and extra fields of this entry to
// simplify alignment logic below and for consistency with
// how compressed entries are handled later.
outEntry
.
setComment
(
null
);
outEntry
.
setExtra
(
null
);
// 'offset' is the offset into the file at which we expect
// 'offset' is the offset into the file at which we expect
// the file data to begin. This is the value we need to
// the file data to begin. This is the value we need to
// make a multiple of 'alignement'.
// make a multiple of 'alignement'.
...
@@ -357,15 +327,18 @@ public class SignAPK {
...
@@ -357,15 +327,18 @@ public class SignAPK {
offset
+=
4
;
offset
+=
4
;
firstEntry
=
false
;
firstEntry
=
false
;
}
}
int
alignment
=
getStoredEntryDataAlignment
(
name
,
defaultAlignment
);
if
(
alignment
>
0
&&
(
offset
%
alignment
!=
0
))
{
if
(
alignment
>
0
&&
(
offset
%
alignment
!=
0
))
{
// Set the "extra data" of the entry to between 1 and
// Set the "extra data" of the entry to between 1 and
// alignment-1 bytes, to make the file data begin at
// alignment-1 bytes, to make the file data begin at
// an aligned offset.
// an aligned offset.
int
needed
=
alignment
-
(
int
)(
offset
%
alignment
);
int
needed
=
alignment
-
(
int
)
(
offset
%
alignment
);
outEntry
.
setExtra
(
new
byte
[
needed
]);
outEntry
.
setExtra
(
new
byte
[
needed
]);
offset
+=
needed
;
offset
+=
needed
;
}
}
out
.
putNextEntry
(
outEntry
);
out
.
putNextEntry
(
outEntry
);
InputStream
data
=
in
.
getInputStream
(
inEntry
);
InputStream
data
=
in
.
getInputStream
(
inEntry
);
while
((
num
=
data
.
read
(
buffer
))
>
0
)
{
while
((
num
=
data
.
read
(
buffer
))
>
0
)
{
out
.
write
(
buffer
,
0
,
num
);
out
.
write
(
buffer
,
0
,
num
);
...
@@ -373,17 +346,20 @@ public class SignAPK {
...
@@ -373,17 +346,20 @@ public class SignAPK {
}
}
out
.
flush
();
out
.
flush
();
}
}
// Copy all the non-STORED entries. We don't attempt to
// Copy all the non-STORED entries. We don't attempt to
// maintain the 'offset' variable past this point; we don't do
// maintain the 'offset' variable past this point; we don't do
// alignment on these entries.
// alignment on these entries.
for
(
String
name
:
names
)
{
for
(
String
name
:
names
)
{
JarEntry
inEntry
=
in
.
getJarEntry
(
name
);
JarEntry
inEntry
=
in
.
getJarEntry
(
name
);
JarEntry
outEntry
=
null
;
JarEntry
outEntry
;
if
(
inEntry
.
getMethod
()
==
JarEntry
.
STORED
)
continue
;
if
(
inEntry
.
getMethod
()
==
JarEntry
.
STORED
)
continue
;
// Create a new entry so that the compressed len is recomputed.
// Create a new entry so that the compressed len is recomputed.
outEntry
=
new
JarEntry
(
name
);
outEntry
=
new
JarEntry
(
name
);
outEntry
.
setTime
(
timestamp
);
outEntry
.
setTime
(
timestamp
);
out
.
putNextEntry
(
outEntry
);
out
.
putNextEntry
(
outEntry
);
InputStream
data
=
in
.
getInputStream
(
inEntry
);
InputStream
data
=
in
.
getInputStream
(
inEntry
);
while
((
num
=
data
.
read
(
buffer
))
>
0
)
{
while
((
num
=
data
.
read
(
buffer
))
>
0
)
{
out
.
write
(
buffer
,
0
,
num
);
out
.
write
(
buffer
,
0
,
num
);
...
@@ -392,135 +368,203 @@ public class SignAPK {
...
@@ -392,135 +368,203 @@ public class SignAPK {
}
}
}
}
// This class is to provide a file's content, but trimming out the last two bytes
/**
// Used for signWholeFile
* Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start
private
static
class
CMSProcessableFile
implements
CMSTypedData
{
* relative to start of file or {@code 0} if alignment of this entry's data is not important.
*/
private
ASN1ObjectIdentifier
type
;
private
static
int
getStoredEntryDataAlignment
(
String
entryName
,
int
defaultAlignment
)
{
private
RandomAccessFile
file
;
if
(
defaultAlignment
<=
0
)
{
return
0
;
}
CMSProcessableFile
(
File
file
)
throws
FileNotFoundException
{
if
(
entryName
.
endsWith
(
".so"
))
{
this
.
file
=
new
RandomAccessFile
(
file
,
"r"
);
// Align .so contents to memory page boundary to enable memory-mapped
type
=
new
ASN1ObjectIdentifier
(
CMSObjectIdentifiers
.
data
.
getId
());
// execution.
return
4096
;
}
else
{
return
defaultAlignment
;
}
}
}
@Override
private
static
void
signFile
(
Manifest
manifest
,
public
ASN1ObjectIdentifier
getContentType
()
{
X509Certificate
[]
publicKey
,
PrivateKey
[]
privateKey
,
return
type
;
long
timestamp
,
JarOutputStream
outputJar
)
throws
Exception
{
// MANIFEST.MF
JarEntry
je
=
new
JarEntry
(
JarFile
.
MANIFEST_NAME
);
je
.
setTime
(
timestamp
);
outputJar
.
putNextEntry
(
je
);
manifest
.
write
(
outputJar
);
int
numKeys
=
publicKey
.
length
;
for
(
int
k
=
0
;
k
<
numKeys
;
++
k
)
{
// CERT.SF / CERT#.SF
je
=
new
JarEntry
(
numKeys
==
1
?
CERT_SF_NAME
:
(
String
.
format
(
Locale
.
US
,
CERT_SF_MULTI_NAME
,
k
)));
je
.
setTime
(
timestamp
);
outputJar
.
putNextEntry
(
je
);
ByteArrayOutputStream
baos
=
new
ByteArrayOutputStream
();
writeSignatureFile
(
manifest
,
baos
,
getDigestAlgorithm
(
publicKey
[
k
]));
byte
[]
signedData
=
baos
.
toByteArray
();
outputJar
.
write
(
signedData
);
// CERT.{EC,RSA} / CERT#.{EC,RSA}
final
String
keyType
=
publicKey
[
k
].
getPublicKey
().
getAlgorithm
();
je
=
new
JarEntry
(
numKeys
==
1
?
(
String
.
format
(
CERT_SIG_NAME
,
keyType
))
:
(
String
.
format
(
Locale
.
US
,
CERT_SIG_MULTI_NAME
,
k
,
keyType
)));
je
.
setTime
(
timestamp
);
outputJar
.
putNextEntry
(
je
);
writeSignatureBlock
(
new
CMSProcessableByteArray
(
signedData
),
publicKey
[
k
],
privateKey
[
k
],
outputJar
);
}
}
}
@Override
/**
public
void
write
(
OutputStream
out
)
throws
IOException
,
CMSException
{
* Converts the provided lists of private keys, their X.509 certificates, and digest algorithms
file
.
seek
(
0
);
* into a list of APK Signature Scheme v2 {@code SignerConfig} instances.
int
read
;
*/
byte
buffer
[]
=
new
byte
[
4096
];
private
static
List
<
ApkSignerV2
.
SignerConfig
>
createV2SignerConfigs
(
int
len
=
(
int
)
file
.
length
()
-
2
;
PrivateKey
[]
privateKeys
,
X509Certificate
[]
certificates
,
String
[]
digestAlgorithms
)
while
((
read
=
file
.
read
(
buffer
,
0
,
len
<
buffer
.
length
?
len
:
buffer
.
length
))
>
0
)
{
throws
InvalidKeyException
{
out
.
write
(
buffer
,
0
,
read
);
if
(
privateKeys
.
length
!=
certificates
.
length
)
{
len
-=
read
;
throw
new
IllegalArgumentException
(
"The number of private keys must match the number of certificates: "
+
privateKeys
.
length
+
" vs"
+
certificates
.
length
);
}
List
<
ApkSignerV2
.
SignerConfig
>
result
=
new
ArrayList
<>(
privateKeys
.
length
);
for
(
int
i
=
0
;
i
<
privateKeys
.
length
;
i
++)
{
PrivateKey
privateKey
=
privateKeys
[
i
];
X509Certificate
certificate
=
certificates
[
i
];
PublicKey
publicKey
=
certificate
.
getPublicKey
();
String
keyAlgorithm
=
privateKey
.
getAlgorithm
();
if
(!
keyAlgorithm
.
equalsIgnoreCase
(
publicKey
.
getAlgorithm
()))
{
throw
new
InvalidKeyException
(
"Key algorithm of private key #"
+
(
i
+
1
)
+
" does not match key"
+
" algorithm of public key #"
+
(
i
+
1
)
+
": "
+
keyAlgorithm
+
" vs "
+
publicKey
.
getAlgorithm
());
}
}
ApkSignerV2
.
SignerConfig
signerConfig
=
new
ApkSignerV2
.
SignerConfig
();
signerConfig
.
privateKey
=
privateKey
;
signerConfig
.
certificates
=
Collections
.
singletonList
(
certificate
);
List
<
Integer
>
signatureAlgorithms
=
new
ArrayList
<>(
digestAlgorithms
.
length
);
for
(
String
digestAlgorithm
:
digestAlgorithms
)
{
try
{
signatureAlgorithms
.
add
(
getV2SignatureAlgorithm
(
keyAlgorithm
,
digestAlgorithm
));
}
catch
(
IllegalArgumentException
e
)
{
throw
new
InvalidKeyException
(
"Unsupported key and digest algorithm combination for signer #"
+
(
i
+
1
),
e
);
}
}
signerConfig
.
signatureAlgorithms
=
signatureAlgorithms
;
result
.
add
(
signerConfig
);
}
}
return
result
;
}
@Override
private
static
int
getV2SignatureAlgorithm
(
String
keyAlgorithm
,
String
digestAlgorithm
)
{
public
Object
getContent
()
{
if
(
"SHA-256"
.
equalsIgnoreCase
(
digestAlgorithm
))
{
return
file
;
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).
return
ApkSignerV2
.
SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256
;
}
else
if
(
"EC"
.
equalsIgnoreCase
(
keyAlgorithm
))
{
return
ApkSignerV2
.
SIGNATURE_ECDSA_WITH_SHA256
;
}
else
if
(
"DSA"
.
equalsIgnoreCase
(
keyAlgorithm
))
{
return
ApkSignerV2
.
SIGNATURE_DSA_WITH_SHA256
;
}
else
{
throw
new
IllegalArgumentException
(
"Unsupported key algorithm: "
+
keyAlgorithm
);
}
}
else
if
(
"SHA-512"
.
equalsIgnoreCase
(
digestAlgorithm
))
{
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).
return
ApkSignerV2
.
SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512
;
}
else
if
(
"EC"
.
equalsIgnoreCase
(
keyAlgorithm
))
{
return
ApkSignerV2
.
SIGNATURE_ECDSA_WITH_SHA512
;
}
else
if
(
"DSA"
.
equalsIgnoreCase
(
keyAlgorithm
))
{
return
ApkSignerV2
.
SIGNATURE_DSA_WITH_SHA512
;
}
else
{
throw
new
IllegalArgumentException
(
"Unsupported key algorithm: "
+
keyAlgorithm
);
}
}
else
{
throw
new
IllegalArgumentException
(
"Unsupported digest algorithm: "
+
digestAlgorithm
);
}
}
}
byte
[]
getTail
()
throws
IOException
{
public
static
void
sign
(
X509Certificate
cert
,
PrivateKey
key
,
byte
tail
[]
=
new
byte
[
22
];
JarMap
inputJar
,
FileOutputStream
outputFile
)
throws
Exception
{
file
.
seek
(
file
.
length
()
-
22
);
int
alignment
=
4
;
file
.
readFully
(
tail
);
int
hashes
=
0
;
return
tail
;
X509Certificate
[]
publicKey
=
new
X509Certificate
[
1
];
publicKey
[
0
]
=
cert
;
hashes
|=
getDigestAlgorithm
(
publicKey
[
0
]);
// Set all ZIP file timestamps to Jan 1 2009 00:00:00.
long
timestamp
=
1230768000000L
;
// The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS
// timestamp using the current timezone. We thus adjust the milliseconds since epoch
// value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00.
timestamp
-=
TimeZone
.
getDefault
().
getOffset
(
timestamp
);
PrivateKey
[]
privateKey
=
new
PrivateKey
[
1
];
privateKey
[
0
]
=
key
;
// Generate, in memory, an APK signed using standard JAR Signature Scheme.
ByteArrayOutputStream
v1SignedApkBuf
=
new
ByteArrayOutputStream
();
JarOutputStream
outputJar
=
new
JarOutputStream
(
v1SignedApkBuf
);
// Use maximum compression for compressed entries because the APK lives forever on
// the system partition.
outputJar
.
setLevel
(
9
);
Manifest
manifest
=
addDigestsToManifest
(
inputJar
,
hashes
);
copyFiles
(
manifest
,
inputJar
,
outputJar
,
timestamp
,
alignment
);
signFile
(
manifest
,
publicKey
,
privateKey
,
timestamp
,
outputJar
);
outputJar
.
close
();
ByteBuffer
v1SignedApk
=
ByteBuffer
.
wrap
(
v1SignedApkBuf
.
toByteArray
());
v1SignedApkBuf
.
reset
();
ByteBuffer
[]
outputChunks
;
List
<
ApkSignerV2
.
SignerConfig
>
signerConfigs
=
createV2SignerConfigs
(
privateKey
,
publicKey
,
new
String
[]{
APK_SIG_SCHEME_V2_DIGEST_ALGORITHM
});
outputChunks
=
ApkSignerV2
.
sign
(
v1SignedApk
,
signerConfigs
);
// This assumes outputChunks are array-backed. To avoid this assumption, the
// code could be rewritten to use FileChannel.
for
(
ByteBuffer
outputChunk
:
outputChunks
)
{
outputFile
.
write
(
outputChunk
.
array
(),
outputChunk
.
arrayOffset
()
+
outputChunk
.
position
(),
outputChunk
.
remaining
());
outputChunk
.
position
(
outputChunk
.
limit
());
}
}
}
}
private
static
void
signWholeFile
(
File
input
,
X509Certificate
publicKey
,
/**
PrivateKey
privateKey
,
OutputStream
outputStream
)
* Write to another stream and track how many bytes have been
throws
Exception
{
* written.
ByteArrayOutputStream
temp
=
new
ByteArrayOutputStream
();
*/
// put a readable message and a null char at the start of the
private
static
class
CountOutputStream
extends
FilterOutputStream
{
// archive comment, so that tools that display the comment
private
int
mCount
;
// (hopefully) show something sensible.
// TODO: anything more useful we can put in this message?
public
CountOutputStream
(
OutputStream
out
)
{
byte
[]
message
=
"signed by SignApk"
.
getBytes
(
"UTF-8"
);
super
(
out
);
temp
.
write
(
message
);
mCount
=
0
;
temp
.
write
(
0
);
CMSProcessableFile
cmsFile
=
new
CMSProcessableFile
(
input
);
writeSignatureBlock
(
cmsFile
,
publicKey
,
privateKey
,
temp
);
// For a zip with no archive comment, the
// end-of-central-directory record will be 22 bytes long, so
// we expect to find the EOCD marker 22 bytes from the end.
byte
[]
zipData
=
cmsFile
.
getTail
();
if
(
zipData
[
zipData
.
length
-
22
]
!=
0x50
||
zipData
[
zipData
.
length
-
21
]
!=
0x4b
||
zipData
[
zipData
.
length
-
20
]
!=
0x05
||
zipData
[
zipData
.
length
-
19
]
!=
0x06
)
{
throw
new
IllegalArgumentException
(
"zip data already has an archive comment"
);
}
}
int
total_size
=
temp
.
size
()
+
6
;
if
(
total_size
>
0xffff
)
{
@Override
throw
new
IllegalArgumentException
(
"signature is too big for ZIP file comment"
);
public
void
write
(
int
b
)
throws
IOException
{
super
.
write
(
b
);
mCount
++;
}
}
// signature starts this many bytes from the end of the file
int
signature_start
=
total_size
-
message
.
length
-
1
;
@Override
temp
.
write
(
signature_start
&
0xff
);
public
void
write
(
byte
[]
b
,
int
off
,
int
len
)
throws
IOException
{
temp
.
write
((
signature_start
>>
8
)
&
0xff
);
super
.
write
(
b
,
off
,
len
);
// Why the 0xff bytes? In a zip file with no archive comment,
mCount
+=
len
;
// bytes [-6:-2] of the file are the little-endian offset from
// the start of the file to the central directory. So for the
// two high bytes to be 0xff 0xff, the archive would have to
// be nearly 4GB in size. So it's unlikely that a real
// commentless archive would have 0xffs here, and lets us tell
// an old signed archive from a new one.
temp
.
write
(
0xff
);
temp
.
write
(
0xff
);
temp
.
write
(
total_size
&
0xff
);
temp
.
write
((
total_size
>>
8
)
&
0xff
);
temp
.
flush
();
// Signature verification checks that the EOCD header is the
// last such sequence in the file (to avoid minzip finding a
// fake EOCD appended after the signature in its scan). The
// odds of producing this sequence by chance are very low, but
// let's catch it here if it does.
byte
[]
b
=
temp
.
toByteArray
();
for
(
int
i
=
0
;
i
<
b
.
length
-
3
;
++
i
)
{
if
(
b
[
i
]
==
0x50
&&
b
[
i
+
1
]
==
0x4b
&&
b
[
i
+
2
]
==
0x05
&&
b
[
i
+
3
]
==
0x06
)
{
throw
new
IllegalArgumentException
(
"found spurious EOCD header at "
+
i
);
}
}
}
cmsFile
.
write
(
outputStream
);
outputStream
.
write
(
total_size
&
0xff
);
outputStream
.
write
((
total_size
>>
8
)
&
0xff
);
temp
.
writeTo
(
outputStream
);
outputStream
.
close
();
}
private
static
void
signFile
(
Manifest
manifest
,
JarMap
inputJar
,
public
int
size
()
{
X509Certificate
cert
,
PrivateKey
privateKey
,
return
mCount
;
JarOutputStream
outputJar
)
}
throws
Exception
{
// Assume the certificate is valid for at least an hour.
long
timestamp
=
cert
.
getNotBefore
().
getTime
()
+
3600L
*
1000
;
// MANIFEST.MF
JarEntry
je
=
new
JarEntry
(
JarFile
.
MANIFEST_NAME
);
je
.
setTime
(
timestamp
);
outputJar
.
putNextEntry
(
je
);
manifest
.
write
(
outputJar
);
je
=
new
JarEntry
(
CERT_SF_NAME
);
je
.
setTime
(
timestamp
);
outputJar
.
putNextEntry
(
je
);
ByteArrayOutputStream
baos
=
new
ByteArrayOutputStream
();
writeSignatureFile
(
manifest
,
baos
,
getDigestAlgorithm
(
cert
));
byte
[]
signedData
=
baos
.
toByteArray
();
outputJar
.
write
(
signedData
);
// CERT.{EC,RSA} / CERT#.{EC,RSA}
final
String
keyType
=
cert
.
getPublicKey
().
getAlgorithm
();
je
=
new
JarEntry
(
String
.
format
(
CERT_SIG_NAME
,
keyType
));
je
.
setTime
(
timestamp
);
outputJar
.
putNextEntry
(
je
);
writeSignatureBlock
(
new
CMSProcessableByteArray
(
signedData
),
cert
,
privateKey
,
outputJar
);
}
}
}
}
app/signing/src/main/java/com/topjohnwu/signing/ZipAdjust.java
deleted
100644 → 0
View file @
fe238839
package
com
.
topjohnwu
.
signing
;
import
java.io.File
;
import
java.io.FileOutputStream
;
import
java.io.IOException
;
import
java.io.OutputStream
;
import
java.io.RandomAccessFile
;
import
java.nio.ByteBuffer
;
import
java.nio.ByteOrder
;
public
class
ZipAdjust
{
public
static
void
adjust
(
File
input
,
File
output
)
throws
IOException
{
try
(
RandomAccessFile
in
=
new
RandomAccessFile
(
input
,
"r"
);
FileOutputStream
out
=
new
FileOutputStream
(
output
)
)
{
adjust
(
in
,
out
);
}
}
public
static
void
adjust
(
RandomAccessFile
in
,
OutputStream
out
)
throws
IOException
{
CentralFooter
footer
=
new
CentralFooter
(
in
);
int
outOff
=
0
;
long
centralOff
=
unsigned
(
footer
.
centralDirectoryOffset
);
CentralHeader
[]
centralHeaders
=
new
CentralHeader
[
unsigned
(
footer
.
numEntries
)];
// Loop through central directory entries
for
(
int
i
=
0
;
i
<
centralHeaders
.
length
;
++
i
)
{
// Read central header
in
.
seek
(
centralOff
);
centralHeaders
[
i
]
=
new
CentralHeader
(
in
);
centralOff
=
in
.
getFilePointer
();
// Read local header
in
.
seek
(
unsigned
(
centralHeaders
[
i
].
localHeaderOffset
));
LocalHeader
localHeader
=
new
LocalHeader
(
in
);
// Make sure local and central headers matches, and strip out data descriptor flag
centralHeaders
[
i
].
localHeaderOffset
=
outOff
;
centralHeaders
[
i
].
flags
&=
~(
1
<<
3
);
localHeader
.
flags
=
centralHeaders
[
i
].
flags
;
localHeader
.
crc32
=
centralHeaders
[
i
].
crc32
;
localHeader
.
compressedSize
=
centralHeaders
[
i
].
compressedSize
;
localHeader
.
uncompressedSize
=
centralHeaders
[
i
].
uncompressedSize
;
localHeader
.
fileNameLength
=
centralHeaders
[
i
].
fileNameLength
;
localHeader
.
filename
=
centralHeaders
[
i
].
filename
;
// Write local header
outOff
+=
localHeader
.
write
(
out
);
// Copy data
int
read
;
long
len
=
unsigned
(
localHeader
.
compressedSize
);
outOff
+=
len
;
byte
data
[]
=
new
byte
[
4096
];
while
((
read
=
in
.
read
(
data
,
0
,
len
<
data
.
length
?
(
int
)
len
:
data
.
length
))
>
0
)
{
out
.
write
(
data
,
0
,
read
);
len
-=
read
;
}
}
footer
.
centralDirectoryOffset
=
outOff
;
// Write central directory
outOff
=
0
;
for
(
CentralHeader
header
:
centralHeaders
)
outOff
+=
header
.
write
(
out
);
// Write central directory record
footer
.
centralDirectorySize
=
outOff
;
footer
.
write
(
out
);
}
public
static
short
unsigned
(
byte
n
)
{
return
(
short
)(
n
&
0xff
);
}
public
static
int
unsigned
(
short
n
)
{
return
n
&
0xffff
;
}
public
static
long
unsigned
(
int
n
)
{
return
n
&
0xffffffff
L
;
}
public
static
class
CentralFooter
{
static
final
int
MAGIC
=
0x06054b50
;
int
signature
;
short
diskNumber
;
short
centralDirectoryDiskNumber
;
short
numEntriesThisDisk
;
short
numEntries
;
int
centralDirectorySize
;
int
centralDirectoryOffset
;
short
zipCommentLength
;
// byte[] comments;
CentralFooter
(
RandomAccessFile
file
)
throws
IOException
{
byte
[]
buffer
=
new
byte
[
22
];
for
(
long
i
=
file
.
length
()
-
4
;
i
>=
0
;
--
i
)
{
file
.
seek
(
i
);
file
.
read
(
buffer
,
0
,
4
);
ByteBuffer
buf
=
ByteBuffer
.
wrap
(
buffer
).
order
(
ByteOrder
.
LITTLE_ENDIAN
);
signature
=
buf
.
getInt
();
if
(
signature
!=
MAGIC
)
{
continue
;
}
file
.
read
(
buffer
,
4
,
buffer
.
length
-
4
);
diskNumber
=
buf
.
getShort
();
centralDirectoryDiskNumber
=
buf
.
getShort
();
numEntriesThisDisk
=
buf
.
getShort
();
numEntries
=
buf
.
getShort
();
centralDirectorySize
=
buf
.
getInt
();
centralDirectoryOffset
=
buf
.
getInt
();
zipCommentLength
=
buf
.
getShort
();
break
;
}
}
int
write
(
OutputStream
out
)
throws
IOException
{
byte
[]
buffer
=
new
byte
[
22
];
ByteBuffer
buf
=
ByteBuffer
.
wrap
(
buffer
).
order
(
ByteOrder
.
LITTLE_ENDIAN
);
buf
.
putInt
(
signature
);
buf
.
putShort
(
diskNumber
);
buf
.
putShort
(
centralDirectoryDiskNumber
);
buf
.
putShort
(
numEntriesThisDisk
);
buf
.
putShort
(
numEntries
);
buf
.
putInt
(
centralDirectorySize
);
buf
.
putInt
(
centralDirectoryOffset
);
buf
.
putShort
((
short
)
0
);
// zipCommentLength
out
.
write
(
buffer
);
return
buffer
.
length
;
}
}
static
class
CentralHeader
{
static
final
int
MAGIC
=
0x02014b50
;
int
signature
;
short
versionMadeBy
;
short
versionNeededToExtract
;
short
flags
;
short
compressionMethod
;
short
lastModFileTime
;
short
lastModFileDate
;
int
crc32
;
int
compressedSize
;
int
uncompressedSize
;
short
fileNameLength
;
short
extraFieldLength
;
short
fileCommentLength
;
short
diskNumberStart
;
short
internalFileAttributes
;
int
externalFileAttributes
;
int
localHeaderOffset
;
byte
[]
filename
;
// byte[] extra;
// byte[] comment;
CentralHeader
(
RandomAccessFile
file
)
throws
IOException
{
byte
[]
buffer
=
new
byte
[
46
];
file
.
read
(
buffer
);
ByteBuffer
buf
=
ByteBuffer
.
wrap
(
buffer
).
order
(
ByteOrder
.
LITTLE_ENDIAN
);
signature
=
buf
.
getInt
();
if
(
signature
!=
MAGIC
)
throw
new
IOException
();
versionMadeBy
=
buf
.
getShort
();
versionNeededToExtract
=
buf
.
getShort
();
flags
=
buf
.
getShort
();
compressionMethod
=
buf
.
getShort
();
lastModFileTime
=
buf
.
getShort
();
lastModFileDate
=
buf
.
getShort
();
crc32
=
buf
.
getInt
();
compressedSize
=
buf
.
getInt
();
uncompressedSize
=
buf
.
getInt
();
fileNameLength
=
buf
.
getShort
();
extraFieldLength
=
buf
.
getShort
();
fileCommentLength
=
buf
.
getShort
();
diskNumberStart
=
buf
.
getShort
();
internalFileAttributes
=
buf
.
getShort
();
externalFileAttributes
=
buf
.
getInt
();
localHeaderOffset
=
buf
.
getInt
();
filename
=
new
byte
[
unsigned
(
fileNameLength
)];
file
.
read
(
filename
);
file
.
skipBytes
(
unsigned
(
extraFieldLength
)
+
unsigned
(
fileCommentLength
));
}
int
write
(
OutputStream
out
)
throws
IOException
{
byte
[]
buffer
=
new
byte
[
46
];
ByteBuffer
buf
=
ByteBuffer
.
wrap
(
buffer
).
order
(
ByteOrder
.
LITTLE_ENDIAN
);
buf
.
putInt
(
signature
);
buf
.
putShort
(
versionMadeBy
);
buf
.
putShort
(
versionNeededToExtract
);
buf
.
putShort
(
flags
);
buf
.
putShort
(
compressionMethod
);
buf
.
putShort
(
lastModFileTime
);
buf
.
putShort
(
lastModFileDate
);
buf
.
putInt
(
crc32
);
buf
.
putInt
(
compressedSize
);
buf
.
putInt
(
uncompressedSize
);
buf
.
putShort
(
fileNameLength
);
buf
.
putShort
((
short
)
0
);
// extraFieldLength
buf
.
putShort
((
short
)
0
);
// fileCommentLength
buf
.
putShort
(
diskNumberStart
);
buf
.
putShort
(
internalFileAttributes
);
buf
.
putInt
(
externalFileAttributes
);
buf
.
putInt
(
localHeaderOffset
);
out
.
write
(
buffer
);
out
.
write
(
filename
);
return
buffer
.
length
+
filename
.
length
;
}
}
static
class
LocalHeader
{
static
final
int
MAGIC
=
0x04034b50
;
int
signature
;
short
versionNeededToExtract
;
short
flags
;
short
compressionMethod
;
short
lastModFileTime
;
short
lastModFileDate
;
int
crc32
;
int
compressedSize
;
int
uncompressedSize
;
short
fileNameLength
;
short
extraFieldLength
;
byte
[]
filename
;
// byte[] extra;
LocalHeader
(
RandomAccessFile
file
)
throws
IOException
{
byte
[]
buffer
=
new
byte
[
30
];
file
.
read
(
buffer
);
ByteBuffer
buf
=
ByteBuffer
.
wrap
(
buffer
).
order
(
ByteOrder
.
LITTLE_ENDIAN
);
signature
=
buf
.
getInt
();
if
(
signature
!=
MAGIC
)
throw
new
IOException
();
versionNeededToExtract
=
buf
.
getShort
();
flags
=
buf
.
getShort
();
compressionMethod
=
buf
.
getShort
();
lastModFileTime
=
buf
.
getShort
();
lastModFileDate
=
buf
.
getShort
();
crc32
=
buf
.
getInt
();
compressedSize
=
buf
.
getInt
();
uncompressedSize
=
buf
.
getInt
();
fileNameLength
=
buf
.
getShort
();
extraFieldLength
=
buf
.
getShort
();
file
.
skipBytes
(
unsigned
(
fileNameLength
)
+
unsigned
(
extraFieldLength
));
}
int
write
(
OutputStream
out
)
throws
IOException
{
byte
[]
buffer
=
new
byte
[
30
];
ByteBuffer
buf
=
ByteBuffer
.
wrap
(
buffer
).
order
(
ByteOrder
.
LITTLE_ENDIAN
);
buf
.
putInt
(
signature
);
buf
.
putShort
(
versionNeededToExtract
);
buf
.
putShort
(
flags
);
buf
.
putShort
(
compressionMethod
);
buf
.
putShort
(
lastModFileTime
);
buf
.
putShort
(
lastModFileDate
);
buf
.
putInt
(
crc32
);
buf
.
putInt
(
compressedSize
);
buf
.
putInt
(
uncompressedSize
);
buf
.
putShort
(
fileNameLength
);
buf
.
putShort
((
short
)
0
);
// extraFieldLength
out
.
write
(
buffer
);
out
.
write
(
filename
);
return
buffer
.
length
+
filename
.
length
;
}
}
}
app/signing/src/main/java/com/topjohnwu/signing/ZipSigner.java
View file @
2e95d9f0
...
@@ -6,7 +6,6 @@ import java.io.FileInputStream;
...
@@ -6,7 +6,6 @@ import java.io.FileInputStream;
import
java.io.FileOutputStream
;
import
java.io.FileOutputStream
;
import
java.io.IOException
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.io.InputStream
;
import
java.io.OutputStream
;
import
java.security.KeyStore
;
import
java.security.KeyStore
;
import
java.security.KeyStoreException
;
import
java.security.KeyStoreException
;
import
java.security.NoSuchAlgorithmException
;
import
java.security.NoSuchAlgorithmException
;
...
@@ -28,20 +27,20 @@ public class ZipSigner {
...
@@ -28,20 +27,20 @@ public class ZipSigner {
System
.
exit
(
2
);
System
.
exit
(
2
);
}
}
private
static
void
sign
(
JarMap
input
,
OutputStream
output
)
throws
Exception
{
private
static
void
sign
(
JarMap
input
,
File
OutputStream
output
)
throws
Exception
{
sign
(
SignA
PK
.
class
.
getResourceAsStream
(
"/keys/testkey.x509.pem"
),
sign
(
SignA
pk
.
class
.
getResourceAsStream
(
"/keys/testkey.x509.pem"
),
SignA
PK
.
class
.
getResourceAsStream
(
"/keys/testkey.pk8"
),
input
,
output
);
SignA
pk
.
class
.
getResourceAsStream
(
"/keys/testkey.pk8"
),
input
,
output
);
}
}
private
static
void
sign
(
InputStream
certIs
,
InputStream
keyIs
,
private
static
void
sign
(
InputStream
certIs
,
InputStream
keyIs
,
JarMap
input
,
OutputStream
output
)
throws
Exception
{
JarMap
input
,
File
OutputStream
output
)
throws
Exception
{
X509Certificate
cert
=
CryptoUtils
.
readCertificate
(
certIs
);
X509Certificate
cert
=
CryptoUtils
.
readCertificate
(
certIs
);
PrivateKey
key
=
CryptoUtils
.
readPrivateKey
(
keyIs
);
PrivateKey
key
=
CryptoUtils
.
readPrivateKey
(
keyIs
);
SignA
PK
.
signAndAdjust
(
cert
,
key
,
input
,
output
);
SignA
pk
.
sign
(
cert
,
key
,
input
,
output
);
}
}
private
static
void
sign
(
String
keyStore
,
String
keyStorePass
,
String
alias
,
String
keyPass
,
private
static
void
sign
(
String
keyStore
,
String
keyStorePass
,
String
alias
,
String
keyPass
,
JarMap
in
,
OutputStream
out
)
throws
Exception
{
JarMap
in
,
File
OutputStream
out
)
throws
Exception
{
KeyStore
ks
;
KeyStore
ks
;
try
{
try
{
ks
=
KeyStore
.
getInstance
(
"JKS"
);
ks
=
KeyStore
.
getInstance
(
"JKS"
);
...
@@ -56,7 +55,7 @@ public class ZipSigner {
...
@@ -56,7 +55,7 @@ public class ZipSigner {
}
}
X509Certificate
cert
=
(
X509Certificate
)
ks
.
getCertificate
(
alias
);
X509Certificate
cert
=
(
X509Certificate
)
ks
.
getCertificate
(
alias
);
PrivateKey
key
=
(
PrivateKey
)
ks
.
getKey
(
alias
,
keyPass
.
toCharArray
());
PrivateKey
key
=
(
PrivateKey
)
ks
.
getKey
(
alias
,
keyPass
.
toCharArray
());
SignA
PK
.
signAndAdjust
(
cert
,
key
,
in
,
out
);
SignA
pk
.
sign
(
cert
,
key
,
in
,
out
);
}
}
public
static
void
main
(
String
[]
args
)
throws
Exception
{
public
static
void
main
(
String
[]
args
)
throws
Exception
{
...
@@ -66,7 +65,7 @@ public class ZipSigner {
...
@@ -66,7 +65,7 @@ public class ZipSigner {
Security
.
insertProviderAt
(
new
BouncyCastleProvider
(),
1
);
Security
.
insertProviderAt
(
new
BouncyCastleProvider
(),
1
);
try
(
JarMap
in
=
JarMap
.
open
(
args
[
args
.
length
-
2
],
false
);
try
(
JarMap
in
=
JarMap
.
open
(
args
[
args
.
length
-
2
],
false
);
OutputStream
out
=
new
FileOutputStream
(
args
[
args
.
length
-
1
]))
{
File
OutputStream
out
=
new
FileOutputStream
(
args
[
args
.
length
-
1
]))
{
if
(
args
.
length
==
2
)
{
if
(
args
.
length
==
2
)
{
sign
(
in
,
out
);
sign
(
in
,
out
);
}
else
if
(
args
.
length
==
4
)
{
}
else
if
(
args
.
length
==
4
)
{
...
...
app/signing/src/main/java/com/topjohnwu/signing/ZipUtils.java
0 → 100644
View file @
2e95d9f0
package
com
.
topjohnwu
.
signing
;
import
java.nio.ByteBuffer
;
import
java.nio.ByteOrder
;
/**
* 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
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_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
ZIP64_EOCD_LOCATOR_SIZE
=
20
;
private
static
final
int
ZIP64_EOCD_LOCATOR_SIG
=
0x07064b50
;
private
static
final
int
UINT16_MAX_VALUE
=
0xffff
;
private
ZipUtils
()
{
}
/**
* 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.
*/
public
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
;
}
/**
* Returns {@code true} if the provided buffer contains a ZIP64 End of Central Directory
* Locator.
*
* <p>NOTE: Byte order of {@code zipContents} must be little-endian.
*/
public
static
boolean
isZip64EndOfCentralDirectoryLocatorPresent
(
ByteBuffer
zipContents
,
int
zipEndOfCentralDirectoryPosition
)
{
assertByteOrderLittleEndian
(
zipContents
);
// ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
// Directory Record.
int
locatorPosition
=
zipEndOfCentralDirectoryPosition
-
ZIP64_EOCD_LOCATOR_SIZE
;
if
(
locatorPosition
<
0
)
{
return
false
;
}
return
zipContents
.
getInt
(
locatorPosition
)
==
ZIP64_EOCD_LOCATOR_SIG
;
}
/**
* 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
);
}
/**
* 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 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
);
}
private
static
void
assertByteOrderLittleEndian
(
ByteBuffer
buffer
)
{
if
(
buffer
.
order
()
!=
ByteOrder
.
LITTLE_ENDIAN
)
{
throw
new
IllegalArgumentException
(
"ByteBuffer byte order must be little endian"
);
}
}
private
static
int
getUnsignedInt16
(
ByteBuffer
buffer
,
int
offset
)
{
return
buffer
.
getShort
(
offset
)
&
0xffff
;
}
private
static
long
getUnsignedInt32
(
ByteBuffer
buffer
,
int
offset
)
{
return
buffer
.
getInt
(
offset
)
&
0xffffffff
L
;
}
private
static
void
setUnsignedInt32
(
ByteBuffer
buffer
,
int
offset
,
long
value
)
{
if
((
value
<
0
)
||
(
value
>
0xffffffff
L
))
{
throw
new
IllegalArgumentException
(
"uint32 value of out range: "
+
value
);
}
buffer
.
putInt
(
buffer
.
position
()
+
offset
,
(
int
)
value
);
}
}
app/src/main/java/com/topjohnwu/magisk/core/utils/Keygen.kt
View file @
2e95d9f0
...
@@ -39,7 +39,7 @@ class Keygen(context: Context) : CertKeyProvider {
...
@@ -39,7 +39,7 @@ class Keygen(context: Context) : CertKeyProvider {
}
}
private
val
start
=
Calendar
.
getInstance
().
apply
{
add
(
Calendar
.
MONTH
,
-
3
)
}
private
val
start
=
Calendar
.
getInstance
().
apply
{
add
(
Calendar
.
MONTH
,
-
3
)
}
private
val
end
=
Calendar
.
getInstance
()
.
apply
{
add
(
Calendar
.
YEAR
,
30
)
}
private
val
end
=
start
.
apply
{
add
(
Calendar
.
YEAR
,
30
)
}
override
val
cert
get
()
=
provider
.
cert
override
val
cert
get
()
=
provider
.
cert
override
val
key
get
()
=
provider
.
key
override
val
key
get
()
=
provider
.
key
...
...
app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt
View file @
2e95d9f0
...
@@ -13,7 +13,7 @@ import com.topjohnwu.magisk.data.network.GithubRawServices
...
@@ -13,7 +13,7 @@ import com.topjohnwu.magisk.data.network.GithubRawServices
import
com.topjohnwu.magisk.ktx.get
import
com.topjohnwu.magisk.ktx.get
import
com.topjohnwu.magisk.ktx.writeTo
import
com.topjohnwu.magisk.ktx.writeTo
import
com.topjohnwu.signing.JarMap
import
com.topjohnwu.signing.JarMap
import
com.topjohnwu.signing.SignA
PK
import
com.topjohnwu.signing.SignA
pk
import
com.topjohnwu.superuser.Shell
import
com.topjohnwu.superuser.Shell
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.GlobalScope
import
kotlinx.coroutines.GlobalScope
...
@@ -29,10 +29,8 @@ import java.security.SecureRandom
...
@@ -29,10 +29,8 @@ import java.security.SecureRandom
object
PatchAPK
{
object
PatchAPK
{
private
const
val
ALPHA
=
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
private
const
val
ALPHA
=
"abcdefghijklmnopqrstuvwxyz"
private
const
val
DIGITS
=
"0123456789"
private
const
val
ALPHADOTS
=
"$ALPHA....."
const
val
ALPHANUM
=
ALPHA
+
DIGITS
private
const
val
ALPHANUMDOTS
=
"$ALPHANUM............"
private
const
val
APP_ID
=
"com.topjohnwu.magisk"
private
const
val
APP_ID
=
"com.topjohnwu.magisk"
private
const
val
APP_NAME
=
"Magisk Manager"
private
const
val
APP_NAME
=
"Magisk Manager"
...
@@ -48,7 +46,7 @@ object PatchAPK {
...
@@ -48,7 +46,7 @@ object PatchAPK {
next
=
if
(
prev
==
'.'
||
i
==
len
-
1
)
{
next
=
if
(
prev
==
'.'
||
i
==
len
-
1
)
{
ALPHA
[
random
.
nextInt
(
ALPHA
.
length
)]
ALPHA
[
random
.
nextInt
(
ALPHA
.
length
)]
}
else
{
}
else
{
ALPHA
NUMDOTS
[
random
.
nextInt
(
ALPHANUM
DOTS
.
length
)]
ALPHA
DOTS
[
random
.
nextInt
(
ALPHA
DOTS
.
length
)]
}
}
builder
.
append
(
next
)
builder
.
append
(
next
)
prev
=
next
prev
=
next
...
@@ -96,7 +94,7 @@ object PatchAPK {
...
@@ -96,7 +94,7 @@ object PatchAPK {
// Write apk changes
// Write apk changes
jar
.
getOutputStream
(
je
).
write
(
xml
)
jar
.
getOutputStream
(
je
).
write
(
xml
)
val
keys
=
Keygen
(
get
())
val
keys
=
Keygen
(
get
())
SignA
PK
.
sign
(
keys
.
cert
,
keys
.
key
,
jar
,
FileOutputStream
(
out
).
buffered
(
))
SignA
pk
.
sign
(
keys
.
cert
,
keys
.
key
,
jar
,
FileOutputStream
(
out
))
}
catch
(
e
:
Exception
)
{
}
catch
(
e
:
Exception
)
{
Timber
.
e
(
e
)
Timber
.
e
(
e
)
return
false
return
false
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment