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
d1c939f4
Commit
d1c939f4
authored
Feb 15, 2017
by
topjohnwu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use temporary files to process zips
Fix #96
parent
21b11f1b
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
882 additions
and
530 deletions
+882
-530
ProcessRepoZip.java
...main/java/com/topjohnwu/magisk/asyncs/ProcessRepoZip.java
+50
-13
ZipUtils.java
app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.java
+785
-510
jni_glue.c
app/src/main/jni/jni_glue.c
+40
-1
zipadjust.c
app/src/main/jni/zipadjust.c
+0
-6
zipadjust.h
app/src/main/jni/zipadjust.h
+7
-0
No files found.
app/src/main/java/com/topjohnwu/magisk/asyncs/ProcessRepoZip.java
View file @
d1c939f4
...
@@ -12,6 +12,10 @@ import com.topjohnwu.magisk.utils.Shell;
...
@@ -12,6 +12,10 @@ import com.topjohnwu.magisk.utils.Shell;
import
com.topjohnwu.magisk.utils.Utils
;
import
com.topjohnwu.magisk.utils.Utils
;
import
com.topjohnwu.magisk.utils.ZipUtils
;
import
com.topjohnwu.magisk.utils.ZipUtils
;
import
java.io.File
;
import
java.io.FileInputStream
;
import
java.io.FileNotFoundException
;
import
java.io.FileOutputStream
;
import
java.io.OutputStream
;
import
java.io.OutputStream
;
public
class
ProcessRepoZip
extends
ParallelTask
<
Void
,
Void
,
Boolean
>
{
public
class
ProcessRepoZip
extends
ParallelTask
<
Void
,
Void
,
Boolean
>
{
...
@@ -33,26 +37,59 @@ public class ProcessRepoZip extends ParallelTask<Void, Void, Boolean> {
...
@@ -33,26 +37,59 @@ public class ProcessRepoZip extends ParallelTask<Void, Void, Boolean> {
@Override
@Override
protected
Boolean
doInBackground
(
Void
...
params
)
{
protected
Boolean
doInBackground
(
Void
...
params
)
{
// Create a buffer in memory for input/output
ByteArrayInOutStream
buffer
=
new
ByteArrayInOutStream
();
FileInputStream
in
;
FileOutputStream
out
;
try
{
try
{
// First remove top folder (the folder with the repo name) in Github source zip
ZipUtils
.
removeTopFolder
(
activity
.
getContentResolver
().
openInputStream
(
mUri
),
buffer
);
// Then sign the zip for the first time
// Create temp file
ZipUtils
.
signZip
(
activity
,
buffer
.
getInputStream
(),
buffer
,
false
);
File
temp1
=
new
File
(
magiskManager
.
getCacheDir
(),
"1.zip"
);
File
temp2
=
new
File
(
magiskManager
.
getCacheDir
(),
"2.zip"
);
if
(
magiskManager
.
getCacheDir
().
mkdirs
())
{
temp1
.
createNewFile
();
temp2
.
createNewFile
();
}
out
=
new
FileOutputStream
(
temp1
);
// First remove top folder in Github source zip, Uri -> temp1
ZipUtils
.
removeTopFolder
(
activity
.
getContentResolver
().
openInputStream
(
mUri
),
out
);
out
.
flush
();
out
.
close
();
out
=
new
FileOutputStream
(
temp2
);
// Adjust the zip to prevent unzip issues
// Then sign the zip for the first time, temp1 -> temp2
ZipUtils
.
adjustZip
(
buffer
);
ZipUtils
.
signZip
(
activity
,
temp1
,
out
,
false
);
out
.
flush
();
out
.
close
();
//
Finally, sign the whole zip file again
//
Adjust the zip to prevent unzip issues, temp2 -> temp2
ZipUtils
.
signZip
(
activity
,
buffer
.
getInputStream
(),
buffer
,
true
);
ZipUtils
.
adjustZip
(
temp2
);
// Write it back to the downloaded zip
out
=
new
FileOutputStream
(
temp1
);
try
(
OutputStream
out
=
activity
.
getContentResolver
().
openOutputStream
(
mUri
))
{
buffer
.
writeTo
(
out
);
// Finally, sign the whole zip file again, temp2 -> temp1
ZipUtils
.
signZip
(
activity
,
temp2
,
out
,
true
);
out
.
flush
();
out
.
close
();
in
=
new
FileInputStream
(
temp1
);
// Write it back to the downloaded zip, temp1 -> Uri
try
(
OutputStream
target
=
activity
.
getContentResolver
().
openOutputStream
(
mUri
))
{
byte
[]
buffer
=
new
byte
[
4096
];
int
length
;
if
(
target
==
null
)
throw
new
FileNotFoundException
();
while
((
length
=
in
.
read
(
buffer
))
>
0
)
target
.
write
(
buffer
,
0
,
length
);
}
}
// Delete the temp file
temp1
.
delete
();
temp2
.
delete
();
return
true
;
return
true
;
}
catch
(
Exception
e
)
{
}
catch
(
Exception
e
)
{
Logger
.
error
(
"ProcessRepoZip: Error!"
);
Logger
.
error
(
"ProcessRepoZip: Error!"
);
...
...
app/src/main/java/com/topjohnwu/magisk/utils/ZipUtils.java
View file @
d1c939f4
...
@@ -76,6 +76,11 @@ public class ZipUtils {
...
@@ -76,6 +76,11 @@ public class ZipUtils {
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
;
// 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
)
+
")$"
);
static
{
static
{
System
.
loadLibrary
(
"zipadjust"
);
System
.
loadLibrary
(
"zipadjust"
);
sBouncyCastleProvider
=
new
BouncyCastleProvider
();
sBouncyCastleProvider
=
new
BouncyCastleProvider
();
...
@@ -84,11 +89,17 @@ public class ZipUtils {
...
@@ -84,11 +89,17 @@ public class ZipUtils {
public
native
static
byte
[]
zipAdjust
(
byte
[]
bytes
,
int
size
);
public
native
static
byte
[]
zipAdjust
(
byte
[]
bytes
,
int
size
);
public
native
static
void
zipAdjust
(
String
filename
);
// Wrapper function for the JNI function
// Wrapper function for the JNI function
public
static
void
adjustZip
(
ByteArrayInOutStream
buffer
)
{
public
static
void
adjustZip
(
ByteArrayInOutStream
buffer
)
{
buffer
.
setBuffer
(
zipAdjust
(
buffer
.
toByteArray
(),
buffer
.
size
()));
buffer
.
setBuffer
(
zipAdjust
(
buffer
.
toByteArray
(),
buffer
.
size
()));
}
}
public
static
void
adjustZip
(
File
file
)
{
zipAdjust
(
file
.
getPath
());
}
public
static
void
removeTopFolder
(
InputStream
in
,
OutputStream
out
)
throws
IOException
{
public
static
void
removeTopFolder
(
InputStream
in
,
OutputStream
out
)
throws
IOException
{
try
{
try
{
JarInputStream
source
=
new
JarInputStream
(
in
);
JarInputStream
source
=
new
JarInputStream
(
in
);
...
@@ -186,24 +197,23 @@ public class ZipUtils {
...
@@ -186,24 +197,23 @@ public class ZipUtils {
}
}
}
}
public
static
void
signZip
(
Context
context
,
InputStream
inputStream
,
public
static
void
signZip
(
Context
context
,
File
input
,
OutputStream
outputStream
,
boolean
signWholeFile
)
throws
Exception
{
OutputStream
outputStream
,
boolean
signWholeFile
)
throws
Exception
{
Jar
Map
inputJar
;
Jar
File
inputJar
=
new
JarFile
(
input
)
;
int
hashes
=
0
;
int
hashes
=
0
;
try
{
try
{
X509Certificate
publicKey
=
readPublicKey
(
context
.
getAssets
().
open
(
PUBLIC_KEY_NAME
));
X509Certificate
publicKey
=
GeneralUtils
.
readPublicKey
(
context
.
getAssets
().
open
(
PUBLIC_KEY_NAME
));
hashes
|=
getDigestAlgorithm
(
publicKey
);
hashes
|=
FileUtils
.
getDigestAlgorithm
(
publicKey
);
// Set the ZIP file timestamp to the starting valid time
// Set the ZIP file timestamp to the starting valid time
// of the 0th certificate plus one hour (to match what
// of the 0th certificate plus one hour (to match what
// we've historically done).
// we've historically done).
long
timestamp
=
publicKey
.
getNotBefore
().
getTime
()
+
3600L
*
1000
;
long
timestamp
=
publicKey
.
getNotBefore
().
getTime
()
+
3600L
*
1000
;
PrivateKey
privateKey
=
readPrivateKey
(
context
.
getAssets
().
open
(
PRIVATE_KEY_NAME
));
PrivateKey
privateKey
=
GeneralUtils
.
readPrivateKey
(
context
.
getAssets
().
open
(
PRIVATE_KEY_NAME
));
inputJar
=
new
JarMap
(
new
JarInputStream
(
inputStream
));
if
(
signWholeFile
)
{
if
(
signWholeFile
)
{
if
(!
"RSA"
.
equalsIgnoreCase
(
privateKey
.
getAlgorithm
()))
{
if
(!
"RSA"
.
equalsIgnoreCase
(
privateKey
.
getAlgorithm
()))
{
throw
new
IOException
(
"Cannot sign OTA packages with non-RSA keys"
);
throw
new
IOException
(
"Cannot sign OTA packages with non-RSA keys"
);
}
}
signWholeFile
(
inputJar
,
context
.
getAssets
().
open
(
PUBLIC_KEY_NAME
),
FileUtils
.
signWholeFile
(
inputJar
,
context
.
getAssets
().
open
(
PUBLIC_KEY_NAME
),
publicKey
,
privateKey
,
outputStream
);
publicKey
,
privateKey
,
outputStream
);
}
else
{
}
else
{
JarOutputStream
outputJar
=
new
JarOutputStream
(
outputStream
);
JarOutputStream
outputJar
=
new
JarOutputStream
(
outputStream
);
...
@@ -214,9 +224,9 @@ public class ZipUtils {
...
@@ -214,9 +224,9 @@ public class ZipUtils {
// and produces output that is only a tiny bit larger
// and produces output that is only a tiny bit larger
// (~0.1% on full OTA packages I tested).
// (~0.1% on full OTA packages I tested).
outputJar
.
setLevel
(
9
);
outputJar
.
setLevel
(
9
);
Manifest
manifest
=
addDigestsToManifest
(
inputJar
,
hashes
);
Manifest
manifest
=
FileUtils
.
addDigestsToManifest
(
inputJar
,
hashes
);
copyFiles
(
manifest
,
inputJar
,
outputJar
,
timestamp
);
FileUtils
.
copyFiles
(
manifest
,
inputJar
,
outputJar
,
timestamp
);
signFile
(
manifest
,
inputJar
,
publicKey
,
privateKey
,
outputJar
);
GeneralUtils
.
signFile
(
manifest
,
publicKey
,
privateKey
,
outputJar
);
outputJar
.
close
();
outputJar
.
close
();
}
}
}
catch
(
Exception
e
)
{
}
catch
(
Exception
e
)
{
...
@@ -225,557 +235,822 @@ public class ZipUtils {
...
@@ -225,557 +235,822 @@ public class ZipUtils {
}
}
}
}
public
static
class
JarMap
extends
TreeMap
<
String
,
Pair
<
JarEntry
,
ByteArrayOutputStream
>
>
{
public
static
void
signZip
(
Context
context
,
InputStream
inputStream
,
OutputStream
outputStream
,
boolean
signWholeFile
)
throws
Exception
{
StreamUtils
.
JarMap
inputJar
;
int
hashes
=
0
;
try
{
X509Certificate
publicKey
=
GeneralUtils
.
readPublicKey
(
context
.
getAssets
().
open
(
PUBLIC_KEY_NAME
));
hashes
|=
FileUtils
.
getDigestAlgorithm
(
publicKey
);
// 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
=
publicKey
.
getNotBefore
().
getTime
()
+
3600L
*
1000
;
PrivateKey
privateKey
=
GeneralUtils
.
readPrivateKey
(
context
.
getAssets
().
open
(
PRIVATE_KEY_NAME
));
inputJar
=
new
StreamUtils
.
JarMap
(
new
JarInputStream
(
inputStream
));
if
(
signWholeFile
)
{
if
(!
"RSA"
.
equalsIgnoreCase
(
privateKey
.
getAlgorithm
()))
{
throw
new
IOException
(
"Cannot sign OTA packages with non-RSA keys"
);
}
StreamUtils
.
signWholeFile
(
inputJar
,
context
.
getAssets
().
open
(
PUBLIC_KEY_NAME
),
publicKey
,
privateKey
,
outputStream
);
}
else
{
JarOutputStream
outputJar
=
new
JarOutputStream
(
outputStream
);
// 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
=
StreamUtils
.
addDigestsToManifest
(
inputJar
,
hashes
);
StreamUtils
.
copyFiles
(
manifest
,
inputJar
,
outputJar
,
timestamp
);
GeneralUtils
.
signFile
(
manifest
,
publicKey
,
privateKey
,
outputJar
);
outputJar
.
close
();
}
}
catch
(
Exception
e
)
{
e
.
printStackTrace
();
throw
e
;
}
}
private
Manifest
manifest
;
// This class host general functions
public
static
class
GeneralUtils
{
public
JarMap
(
JarInputStream
in
)
throws
IOException
{
/** Returns the expected signature algorithm for this key type. */
super
();
private
static
String
getSignatureAlgorithm
(
X509Certificate
cert
)
{
manifest
=
in
.
getManifest
();
String
sigAlg
=
cert
.
getSigAlgName
().
toUpperCase
(
Locale
.
US
);
byte
[]
buffer
=
new
byte
[
4096
];
String
keyType
=
cert
.
getPublicKey
().
getAlgorithm
().
toUpperCase
(
Locale
.
US
);
int
num
;
if
(
"RSA"
.
equalsIgnoreCase
(
keyType
))
{
JarEntry
entry
;
if
(
FileUtils
.
getDigestAlgorithm
(
cert
)
==
USE_SHA256
)
{
while
((
entry
=
in
.
getNextJarEntry
())
!=
null
)
{
return
"SHA256withRSA"
;
ByteArrayOutputStream
stream
=
new
ByteArrayOutputStream
();
}
else
{
while
((
num
=
in
.
read
(
buffer
))
>
0
)
{
return
"SHA1withRSA"
;
stream
.
write
(
buffer
,
0
,
num
);
}
}
put
(
entry
.
getName
(),
entry
,
stream
);
}
else
if
(
"DSA"
.
equalsIgnoreCase
(
keyType
))
{
return
"SHA256withDSA"
;
}
else
if
(
"EC"
.
equalsIgnoreCase
(
keyType
))
{
return
"SHA256withECDSA"
;
}
else
{
throw
new
IllegalArgumentException
(
"unsupported key type: "
+
keyType
);
}
}
in
.
close
();
}
}
public
JarEntry
getJarEntry
(
String
name
)
{
private
static
X509Certificate
readPublicKey
(
InputStream
input
)
return
get
(
name
).
first
;
throws
IOException
,
GeneralSecurityException
{
}
try
{
public
ByteArrayOutputStream
getStream
(
String
name
)
{
CertificateFactory
cf
=
CertificateFactory
.
getInstance
(
"X.509"
);
return
get
(
name
).
second
;
return
(
X509Certificate
)
cf
.
generateCertificate
(
input
);
}
}
finally
{
public
void
put
(
String
name
,
JarEntry
entry
,
ByteArrayOutputStream
stream
)
{
input
.
close
();
put
(
name
,
new
Pair
<>(
entry
,
stream
));
}
}
public
Manifest
getManifest
()
{
return
manifest
;
}
public
Enumeration
<
JarEntry
>
entries
()
{
Iterator
<
Map
.
Entry
<
String
,
Pair
<
JarEntry
,
ByteArrayOutputStream
>
>>
i
=
entrySet
().
iterator
();
ArrayList
<
JarEntry
>
list
=
new
ArrayList
<>();
while
(
i
.
hasNext
())
list
.
add
(
i
.
next
().
getValue
().
first
);
return
Collections
.
enumeration
(
list
);
}
}
}
/**
/**
* Return one of USE_SHA1 or USE_SHA256 according to the signature
* Decrypt an encrypted PKCS 8 format private key.
* algorithm specified in the cert.
*
*/
* Based on ghstark's post on Aug 6, 2006 at
private
static
int
getDigestAlgorithm
(
X509Certificate
cert
)
{
* http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
String
sigAlg
=
cert
.
getSigAlgName
().
toUpperCase
(
Locale
.
US
);
*
if
(
"SHA1WITHRSA"
.
equals
(
sigAlg
)
||
* @param encryptedPrivateKey The raw data of the private key
"MD5WITHRSA"
.
equals
(
sigAlg
))
{
// see "HISTORICAL NOTE" above.
* @param keyFile The file containing the private key
return
USE_SHA1
;
*/
}
else
if
(
sigAlg
.
startsWith
(
"SHA256WITH"
))
{
private
static
KeySpec
decryptPrivateKey
(
byte
[]
encryptedPrivateKey
,
File
keyFile
)
return
USE_SHA256
;
throws
GeneralSecurityException
{
}
else
{
EncryptedPrivateKeyInfo
epkInfo
;
throw
new
IllegalArgumentException
(
"unsupported signature algorithm \""
+
sigAlg
+
try
{
"\" in cert ["
+
cert
.
getSubjectDN
());
epkInfo
=
new
EncryptedPrivateKeyInfo
(
encryptedPrivateKey
);
}
}
catch
(
IOException
ex
)
{
}
// Probably not an encrypted key.
/** Returns the expected signature algorithm for this key type. */
return
null
;
private
static
String
getSignatureAlgorithm
(
X509Certificate
cert
)
{
}
String
sigAlg
=
cert
.
getSigAlgName
().
toUpperCase
(
Locale
.
US
);
// We no longer have console, so need to use another way to input password
String
keyType
=
cert
.
getPublicKey
().
getAlgorithm
().
toUpperCase
(
Locale
.
US
);
// This function is left here if needed in the future, so no use for now
if
(
"RSA"
.
equalsIgnoreCase
(
keyType
))
{
char
[]
password
=
new
char
[
0
];
if
(
getDigestAlgorithm
(
cert
)
==
USE_SHA256
)
{
SecretKeyFactory
skFactory
=
SecretKeyFactory
.
getInstance
(
epkInfo
.
getAlgName
());
return
"SHA256withRSA"
;
Key
key
=
skFactory
.
generateSecret
(
new
PBEKeySpec
(
password
));
}
else
{
Cipher
cipher
=
Cipher
.
getInstance
(
epkInfo
.
getAlgName
());
return
"SHA1withRSA"
;
cipher
.
init
(
Cipher
.
DECRYPT_MODE
,
key
,
epkInfo
.
getAlgParameters
());
}
try
{
}
else
if
(
"DSA"
.
equalsIgnoreCase
(
keyType
))
{
return
epkInfo
.
getKeySpec
(
cipher
);
return
"SHA256withDSA"
;
}
catch
(
InvalidKeySpecException
ex
)
{
}
else
if
(
"EC"
.
equalsIgnoreCase
(
keyType
))
{
System
.
err
.
println
(
"signapk: Password for "
+
keyFile
+
" may be bad."
);
return
"SHA256withECDSA"
;
throw
ex
;
}
else
{
}
throw
new
IllegalArgumentException
(
"unsupported key type: "
+
keyType
);
}
}
// 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
)
+
")$"
);
private
static
X509Certificate
readPublicKey
(
InputStream
input
)
throws
IOException
,
GeneralSecurityException
{
try
{
CertificateFactory
cf
=
CertificateFactory
.
getInstance
(
"X.509"
);
return
(
X509Certificate
)
cf
.
generateCertificate
(
input
);
}
finally
{
input
.
close
();
}
}
}
/**
/** Read a PKCS 8 format private key. */
* Decrypt an encrypted PKCS 8 format private key.
private
static
PrivateKey
readPrivateKey
(
InputStream
input
)
*
throws
IOException
,
GeneralSecurityException
{
* Based on ghstark's post on Aug 6, 2006 at
try
{
* http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
byte
[]
buffer
=
new
byte
[
4096
];
*
int
size
=
input
.
read
(
buffer
);
* @param encryptedPrivateKey The raw data of the private key
byte
[]
bytes
=
Arrays
.
copyOf
(
buffer
,
size
);
* @param keyFile The file containing the private key
KeySpec
spec
=
new
PKCS8EncodedKeySpec
(
bytes
);
*/
PrivateKey
key
;
private
static
KeySpec
decryptPrivateKey
(
byte
[]
encryptedPrivateKey
,
File
keyFile
)
key
=
decodeAsKeyType
(
spec
,
"RSA"
);
throws
GeneralSecurityException
{
if
(
key
!=
null
)
{
EncryptedPrivateKeyInfo
epkInfo
;
return
key
;
try
{
}
epkInfo
=
new
EncryptedPrivateKeyInfo
(
encryptedPrivateKey
);
key
=
decodeAsKeyType
(
spec
,
"DSA"
);
}
catch
(
IOException
ex
)
{
if
(
key
!=
null
)
{
// Probably not an encrypted key.
return
key
;
return
null
;
}
key
=
decodeAsKeyType
(
spec
,
"EC"
);
if
(
key
!=
null
)
{
return
key
;
}
throw
new
NoSuchAlgorithmException
(
"Must be an RSA, DSA, or EC key"
);
}
finally
{
input
.
close
();
}
}
}
// We no longer have console, so need to use another way to input password
// This function is left here if needed in the future, so no use for now
private
static
PrivateKey
decodeAsKeyType
(
KeySpec
spec
,
String
keyType
)
char
[]
password
=
new
char
[
0
];
throws
GeneralSecurityException
{
SecretKeyFactory
skFactory
=
SecretKeyFactory
.
getInstance
(
epkInfo
.
getAlgName
());
try
{
Key
key
=
skFactory
.
generateSecret
(
new
PBEKeySpec
(
password
));
return
KeyFactory
.
getInstance
(
keyType
).
generatePrivate
(
spec
);
Cipher
cipher
=
Cipher
.
getInstance
(
epkInfo
.
getAlgName
());
}
catch
(
InvalidKeySpecException
e
)
{
cipher
.
init
(
Cipher
.
DECRYPT_MODE
,
key
,
epkInfo
.
getAlgParameters
());
return
null
;
try
{
}
return
epkInfo
.
getKeySpec
(
cipher
);
}
catch
(
InvalidKeySpecException
ex
)
{
System
.
err
.
println
(
"signapk: Password for "
+
keyFile
+
" may be bad."
);
throw
ex
;
}
}
}
/** Read a PKCS 8 format private key. */
/**
private
static
PrivateKey
readPrivateKey
(
InputStream
input
)
* Add a copy of the public key to the archive; this should
throws
IOException
,
GeneralSecurityException
{
* exactly match one of the files in
try
{
* /system/etc/security/otacerts.zip on the device. (The same
byte
[]
buffer
=
new
byte
[
4096
];
* cert can be extracted from the CERT.RSA file but this is much
int
size
=
input
.
read
(
buffer
);
* easier to get at.)
byte
[]
bytes
=
Arrays
.
copyOf
(
buffer
,
size
);
*/
KeySpec
spec
=
new
PKCS8EncodedKeySpec
(
bytes
);
private
static
void
addOtacert
(
JarOutputStream
outputJar
,
PrivateKey
key
;
InputStream
input
,
key
=
decodeAsKeyType
(
spec
,
"RSA"
);
long
timestamp
,
if
(
key
!=
null
)
{
Manifest
manifest
,
return
key
;
int
hash
)
}
throws
IOException
,
GeneralSecurityException
{
key
=
decodeAsKeyType
(
spec
,
"DSA
"
);
MessageDigest
md
=
MessageDigest
.
getInstance
(
hash
==
USE_SHA1
?
"SHA1"
:
"SHA256
"
);
if
(
key
!=
null
)
{
JarEntry
je
=
new
JarEntry
(
OTACERT_NAME
);
return
key
;
je
.
setTime
(
timestamp
)
;
}
outputJar
.
putNextEntry
(
je
);
key
=
decodeAsKeyType
(
spec
,
"EC"
)
;
byte
[]
b
=
new
byte
[
4096
]
;
i
f
(
key
!=
null
)
{
i
nt
read
;
return
key
;
while
((
read
=
input
.
read
(
b
))
!=
-
1
)
{
}
outputJar
.
write
(
b
,
0
,
read
);
throw
new
NoSuchAlgorithmException
(
"Must be an RSA, DSA, or EC key"
);
md
.
update
(
b
,
0
,
read
);
}
finally
{
}
input
.
close
();
input
.
close
();
Attributes
attr
=
new
Attributes
();
attr
.
putValue
(
hash
==
USE_SHA1
?
"SHA1-Digest"
:
"SHA-256-Digest"
,
new
String
(
Base64
.
encode
(
md
.
digest
()),
"ASCII"
));
manifest
.
getEntries
().
put
(
OTACERT_NAME
,
attr
);
}
}
}
private
static
PrivateKey
decodeAsKeyType
(
KeySpec
spec
,
String
keyType
)
throws
GeneralSecurityException
{
try
{
return
KeyFactory
.
getInstance
(
keyType
).
generatePrivate
(
spec
);
}
catch
(
InvalidKeySpecException
e
)
{
return
null
;
}
}
/**
/** Write a .SF file with a digest of the specified manifest. */
* Add the hash(es) of every file to the manifest, creating it if
private
static
void
writeSignatureFile
(
Manifest
manifest
,
OutputStream
out
,
* necessary.
int
hash
)
*/
throws
IOException
,
GeneralSecurityException
{
private
static
Manifest
addDigestsToManifest
(
JarMap
jar
,
int
hashes
)
Manifest
sf
=
new
Manifest
();
throws
IOException
,
GeneralSecurityException
{
Attributes
main
=
sf
.
getMainAttributes
();
Manifest
input
=
jar
.
getManifest
();
main
.
putValue
(
"Signature-Version"
,
"1.0"
);
Manifest
output
=
new
Manifest
();
Attributes
main
=
output
.
getMainAttributes
();
if
(
input
!=
null
)
{
main
.
putAll
(
input
.
getMainAttributes
());
}
else
{
main
.
putValue
(
"Manifest-Version"
,
"1.0"
);
main
.
putValue
(
"Created-By"
,
"1.0 (Android SignApk)"
);
main
.
putValue
(
"Created-By"
,
"1.0 (Android SignApk)"
);
}
MessageDigest
md
=
MessageDigest
.
getInstance
(
MessageDigest
md_sha1
=
null
;
hash
==
USE_SHA256
?
"SHA256"
:
"SHA1"
);
MessageDigest
md_sha256
=
null
;
PrintStream
print
=
new
PrintStream
(
if
((
hashes
&
USE_SHA1
)
!=
0
)
{
new
DigestOutputStream
(
new
ByteArrayOutputStream
(),
md
),
md_sha1
=
MessageDigest
.
getInstance
(
"SHA1"
);
true
,
"UTF-8"
);
}
// Digest of the entire manifest
if
((
hashes
&
USE_SHA256
)
!=
0
)
{
manifest
.
write
(
print
);
md_sha256
=
MessageDigest
.
getInstance
(
"SHA256"
);
print
.
flush
();
}
main
.
putValue
(
hash
==
USE_SHA256
?
"SHA-256-Digest-Manifest"
:
"SHA1-Digest-Manifest"
,
// We sort the input entries by name, and add them to the
new
String
(
Base64
.
encode
(
md
.
digest
()),
"ASCII"
));
// output manifest in sorted order. We expect that the output
Map
<
String
,
Attributes
>
entries
=
manifest
.
getEntries
();
// map will be deterministic.
for
(
Map
.
Entry
<
String
,
Attributes
>
entry
:
entries
.
entrySet
())
{
/* JarMap is a TreeMap, so it's already sorted */
// Digest of the manifest stanza for this entry.
for
(
String
name
:
jar
.
keySet
())
{
print
.
print
(
"Name: "
+
entry
.
getKey
()
+
"\r\n"
);
JarEntry
entry
=
jar
.
getJarEntry
(
name
);
for
(
Map
.
Entry
<
Object
,
Object
>
att
:
entry
.
getValue
().
entrySet
())
{
if
(!
entry
.
isDirectory
()
&&
print
.
print
(
att
.
getKey
()
+
": "
+
att
.
getValue
()
+
"\r\n"
);
(
stripPattern
==
null
||
!
stripPattern
.
matcher
(
name
).
matches
()))
{
byte
[]
buffer
=
jar
.
getStream
(
name
).
toByteArray
();
if
(
md_sha1
!=
null
)
md_sha1
.
update
(
buffer
,
0
,
buffer
.
length
);
if
(
md_sha256
!=
null
)
md_sha256
.
update
(
buffer
,
0
,
buffer
.
length
);
Attributes
attr
=
null
;
if
(
input
!=
null
)
attr
=
input
.
getAttributes
(
name
);
attr
=
attr
!=
null
?
new
Attributes
(
attr
)
:
new
Attributes
();
if
(
md_sha1
!=
null
)
{
attr
.
putValue
(
"SHA1-Digest"
,
new
String
(
Base64
.
encode
(
md_sha1
.
digest
()),
"ASCII"
));
}
if
(
md_sha256
!=
null
)
{
attr
.
putValue
(
"SHA-256-Digest"
,
new
String
(
Base64
.
encode
(
md_sha256
.
digest
()),
"ASCII"
));
}
}
output
.
getEntries
().
put
(
name
,
attr
);
print
.
print
(
"\r\n"
);
print
.
flush
();
Attributes
sfAttr
=
new
Attributes
();
sfAttr
.
putValue
(
hash
==
USE_SHA256
?
"SHA-256-Digest"
:
"SHA1-Digest-Manifest"
,
new
String
(
Base64
.
encode
(
md
.
digest
()),
"ASCII"
));
sf
.
getEntries
().
put
(
entry
.
getKey
(),
sfAttr
);
}
CountOutputStream
cout
=
new
CountOutputStream
(
out
);
sf
.
write
(
cout
);
// 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
((
cout
.
size
()
%
1024
)
==
0
)
{
cout
.
write
(
'\r'
);
cout
.
write
(
'\n'
);
}
}
}
}
return
output
;
}
/**
/** Sign data and write the digital signature to 'out'. */
* Add a copy of the public key to the archive; this should
private
static
void
writeSignatureBlock
(
* exactly match one of the files in
CMSTypedData
data
,
X509Certificate
publicKey
,
PrivateKey
privateKey
,
OutputStream
out
)
* /system/etc/security/otacerts.zip on the device. (The same
throws
IOException
,
CertificateEncodingException
,
OperatorCreationException
,
CMSException
{
* cert can be extracted from the CERT.RSA file but this is much
ArrayList
<
X509Certificate
>
certList
=
new
ArrayList
<
X509Certificate
>(
1
);
* easier to get at.)
certList
.
add
(
publicKey
);
*/
JcaCertStore
certs
=
new
JcaCertStore
(
certList
);
private
static
void
addOtacert
(
JarOutputStream
outputJar
,
CMSSignedDataGenerator
gen
=
new
CMSSignedDataGenerator
();
InputStream
input
,
ContentSigner
signer
=
new
JcaContentSignerBuilder
(
getSignatureAlgorithm
(
publicKey
))
long
timestamp
,
.
setProvider
(
sBouncyCastleProvider
)
Manifest
manifest
,
.
build
(
privateKey
);
int
hash
)
gen
.
addSignerInfoGenerator
(
throws
IOException
,
GeneralSecurityException
{
new
JcaSignerInfoGeneratorBuilder
(
MessageDigest
md
=
MessageDigest
.
getInstance
(
hash
==
USE_SHA1
?
"SHA1"
:
"SHA256"
);
new
JcaDigestCalculatorProviderBuilder
()
JarEntry
je
=
new
JarEntry
(
OTACERT_NAME
);
.
setProvider
(
sBouncyCastleProvider
)
je
.
setTime
(
timestamp
);
.
build
())
outputJar
.
putNextEntry
(
je
);
.
setDirectSignature
(
true
)
byte
[]
b
=
new
byte
[
4096
];
.
build
(
signer
,
publicKey
));
int
read
;
gen
.
addCertificates
(
certs
);
while
((
read
=
input
.
read
(
b
))
!=
-
1
)
{
CMSSignedData
sigData
=
gen
.
generate
(
data
,
false
);
outputJar
.
write
(
b
,
0
,
read
);
try
(
ASN1InputStream
asn1
=
new
ASN1InputStream
(
sigData
.
getEncoded
()))
{
md
.
update
(
b
,
0
,
read
);
DEROutputStream
dos
=
new
DEROutputStream
(
out
);
dos
.
writeObject
(
asn1
.
readObject
());
}
}
}
input
.
close
();
Attributes
attr
=
new
Attributes
();
attr
.
putValue
(
hash
==
USE_SHA1
?
"SHA1-Digest"
:
"SHA-256-Digest"
,
new
String
(
Base64
.
encode
(
md
.
digest
()),
"ASCII"
));
manifest
.
getEntries
().
put
(
OTACERT_NAME
,
attr
);
}
/** Write to another stream and track how many bytes have been
private
static
void
signFile
(
Manifest
manifest
,
* written.
X509Certificate
publicKey
,
PrivateKey
privateKey
,
*/
JarOutputStream
outputJar
)
private
static
class
CountOutputStream
extends
FilterOutputStream
{
throws
Exception
{
private
int
mCount
;
// Assume the certificate is valid for at least an hour.
public
CountOutputStream
(
OutputStream
out
)
{
long
timestamp
=
publicKey
.
getNotBefore
().
getTime
()
+
3600L
*
1000
;
super
(
out
);
// MANIFEST.MF
mCount
=
0
;
JarEntry
je
=
new
JarEntry
(
JarFile
.
MANIFEST_NAME
);
}
je
.
setTime
(
timestamp
);
@Override
outputJar
.
putNextEntry
(
je
);
public
void
write
(
int
b
)
throws
IOException
{
manifest
.
write
(
outputJar
);
super
.
write
(
b
);
// CERT.SF / CERT#.SF
mCount
++;
je
=
new
JarEntry
(
CERT_SF_NAME
);
}
je
.
setTime
(
timestamp
);
@Override
outputJar
.
putNextEntry
(
je
);
public
void
write
(
byte
[]
b
,
int
off
,
int
len
)
throws
IOException
{
ByteArrayOutputStream
baos
=
new
ByteArrayOutputStream
();
super
.
write
(
b
,
off
,
len
);
writeSignatureFile
(
manifest
,
baos
,
FileUtils
.
getDigestAlgorithm
(
publicKey
));
mCount
+=
len
;
byte
[]
signedData
=
baos
.
toByteArray
();
}
outputJar
.
write
(
signedData
);
public
int
size
()
{
// CERT.{DSA,EC,RSA} / CERT#.{DSA,EC,RSA}
return
mCount
;
je
=
new
JarEntry
((
String
.
format
(
CERT_SIG_NAME
,
privateKey
.
getAlgorithm
())));
je
.
setTime
(
timestamp
);
outputJar
.
putNextEntry
(
je
);
writeSignatureBlock
(
new
CMSProcessableByteArray
(
signedData
),
publicKey
,
privateKey
,
outputJar
);
}
}
}
/** Write a .SF file with a digest of the specified manifest. */
/** Write to another stream and track how many bytes have been
private
static
void
writeSignatureFile
(
Manifest
manifest
,
OutputStream
out
,
* written.
int
hash
)
*/
throws
IOException
,
GeneralSecurityException
{
private
static
class
CountOutputStream
extends
FilterOutputStream
{
Manifest
sf
=
new
Manifest
();
private
int
mCount
;
Attributes
main
=
sf
.
getMainAttributes
();
public
CountOutputStream
(
OutputStream
out
)
{
main
.
putValue
(
"Signature-Version"
,
"1.0"
);
super
(
out
);
main
.
putValue
(
"Created-By"
,
"1.0 (Android SignApk)"
);
mCount
=
0
;
MessageDigest
md
=
MessageDigest
.
getInstance
(
}
hash
==
USE_SHA256
?
"SHA256"
:
"SHA1"
);
@Override
PrintStream
print
=
new
PrintStream
(
public
void
write
(
int
b
)
throws
IOException
{
new
DigestOutputStream
(
new
ByteArrayOutputStream
(),
md
),
super
.
write
(
b
);
true
,
"UTF-8"
);
mCount
++;
// Digest of the entire manifest
}
manifest
.
write
(
print
);
@Override
print
.
flush
();
public
void
write
(
byte
[]
b
,
int
off
,
int
len
)
throws
IOException
{
main
.
putValue
(
hash
==
USE_SHA256
?
"SHA-256-Digest-Manifest"
:
"SHA1-Digest-Manifest"
,
super
.
write
(
b
,
off
,
len
);
new
String
(
Base64
.
encode
(
md
.
digest
()),
"ASCII"
));
mCount
+=
len
;
Map
<
String
,
Attributes
>
entries
=
manifest
.
getEntries
();
}
for
(
Map
.
Entry
<
String
,
Attributes
>
entry
:
entries
.
entrySet
())
{
public
int
size
()
{
// Digest of the manifest stanza for this entry.
return
mCount
;
print
.
print
(
"Name: "
+
entry
.
getKey
()
+
"\r\n"
);
}
for
(
Map
.
Entry
<
Object
,
Object
>
att
:
entry
.
getValue
().
entrySet
())
{
print
.
print
(
att
.
getKey
()
+
": "
+
att
.
getValue
()
+
"\r\n"
);
}
print
.
print
(
"\r\n"
);
print
.
flush
();
Attributes
sfAttr
=
new
Attributes
();
sfAttr
.
putValue
(
hash
==
USE_SHA256
?
"SHA-256-Digest"
:
"SHA1-Digest-Manifest"
,
new
String
(
Base64
.
encode
(
md
.
digest
()),
"ASCII"
));
sf
.
getEntries
().
put
(
entry
.
getKey
(),
sfAttr
);
}
CountOutputStream
cout
=
new
CountOutputStream
(
out
);
sf
.
write
(
cout
);
// 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
((
cout
.
size
()
%
1024
)
==
0
)
{
cout
.
write
(
'\r'
);
cout
.
write
(
'\n'
);
}
}
}
/** Sign data and write the digital signature to 'out'. */
private
static
class
WholeFileSignerOutputStream
extends
FilterOutputStream
{
private
static
void
writeSignatureBlock
(
private
boolean
closing
=
false
;
CMSTypedData
data
,
X509Certificate
publicKey
,
PrivateKey
privateKey
,
OutputStream
out
)
private
ByteArrayOutputStream
footer
=
new
ByteArrayOutputStream
();
throws
IOException
,
CertificateEncodingException
,
OperatorCreationException
,
CMSException
{
private
OutputStream
tee
;
ArrayList
<
X509Certificate
>
certList
=
new
ArrayList
<
X509Certificate
>(
1
);
public
WholeFileSignerOutputStream
(
OutputStream
out
,
OutputStream
tee
)
{
certList
.
add
(
publicKey
);
super
(
out
);
JcaCertStore
certs
=
new
JcaCertStore
(
certList
);
this
.
tee
=
tee
;
CMSSignedDataGenerator
gen
=
new
CMSSignedDataGenerator
();
}
ContentSigner
signer
=
new
JcaContentSignerBuilder
(
getSignatureAlgorithm
(
publicKey
))
public
void
notifyClosing
()
{
.
setProvider
(
sBouncyCastleProvider
)
closing
=
true
;
.
build
(
privateKey
);
}
gen
.
addSignerInfoGenerator
(
public
void
finish
()
throws
IOException
{
new
JcaSignerInfoGeneratorBuilder
(
closing
=
false
;
new
JcaDigestCalculatorProviderBuilder
()
byte
[]
data
=
footer
.
toByteArray
();
.
setProvider
(
sBouncyCastleProvider
)
if
(
data
.
length
<
2
)
.
build
())
throw
new
IOException
(
"Less than two bytes written to footer"
);
.
setDirectSignature
(
true
)
write
(
data
,
0
,
data
.
length
-
2
);
.
build
(
signer
,
publicKey
));
}
gen
.
addCertificates
(
certs
);
public
byte
[]
getTail
()
{
CMSSignedData
sigData
=
gen
.
generate
(
data
,
false
);
return
footer
.
toByteArray
();
try
(
ASN1InputStream
asn1
=
new
ASN1InputStream
(
sigData
.
getEncoded
()))
{
}
DEROutputStream
dos
=
new
DEROutputStream
(
out
);
@Override
dos
.
writeObject
(
asn1
.
readObject
());
public
void
write
(
byte
[]
b
)
throws
IOException
{
write
(
b
,
0
,
b
.
length
);
}
@Override
public
void
write
(
byte
[]
b
,
int
off
,
int
len
)
throws
IOException
{
if
(
closing
)
{
// if the jar is about to close, save the footer that will be written
footer
.
write
(
b
,
off
,
len
);
}
else
{
// write to both output streams. out is the CMSTypedData signer and tee is the file.
out
.
write
(
b
,
off
,
len
);
tee
.
write
(
b
,
off
,
len
);
}
}
@Override
public
void
write
(
int
b
)
throws
IOException
{
if
(
closing
)
{
// if the jar is about to close, save the footer that will be written
footer
.
write
(
b
);
}
else
{
// write to both output streams. out is the CMSTypedData signer and tee is the file.
out
.
write
(
b
);
tee
.
write
(
b
);
}
}
}
}
}
}
/**
* Copy all the files in a manifest from input to output. We set
// This class host functions that consumes JarFiles
* the modification times in the output to a fixed time, so as to
public
static
class
FileUtils
{
* reduce variation in the output file and make incremental OTAs
* more efficient.
/**
*/
* Return one of USE_SHA1 or USE_SHA256 according to the signature
private
static
void
copyFiles
(
Manifest
manifest
,
* algorithm specified in the cert.
JarMap
in
,
JarOutputStream
out
,
long
timestamp
)
throws
IOException
{
*/
Map
<
String
,
Attributes
>
entries
=
manifest
.
getEntries
();
private
static
int
getDigestAlgorithm
(
X509Certificate
cert
)
{
ArrayList
<
String
>
names
=
new
ArrayList
<>(
entries
.
keySet
());
String
sigAlg
=
cert
.
getSigAlgName
().
toUpperCase
(
Locale
.
US
);
Collections
.
sort
(
names
);
if
(
"SHA1WITHRSA"
.
equals
(
sigAlg
)
||
for
(
String
name
:
names
)
{
"MD5WITHRSA"
.
equals
(
sigAlg
))
{
// see "HISTORICAL NOTE" above.
JarEntry
inEntry
=
in
.
getJarEntry
(
name
);
return
USE_SHA1
;
JarEntry
outEntry
;
}
else
if
(
sigAlg
.
startsWith
(
"SHA256WITH"
))
{
if
(
inEntry
.
getMethod
()
==
JarEntry
.
STORED
)
{
return
USE_SHA256
;
// Preserve the STORED method of the input entry.
outEntry
=
new
JarEntry
(
inEntry
);
}
else
{
}
else
{
// Create a new entry so that the compressed len is recomputed.
throw
new
IllegalArgumentException
(
"unsupported signature algorithm \""
+
sigAlg
+
outEntry
=
new
JarEntry
(
name
);
"\" in cert ["
+
cert
.
getSubjectDN
()
);
}
}
outEntry
.
setTime
(
timestamp
);
out
.
putNextEntry
(
outEntry
);
in
.
getStream
(
name
).
writeTo
(
out
);
out
.
flush
();
}
}
}
private
static
class
WholeFileSignerOutputStream
extends
FilterOutputStream
{
private
static
Manifest
addDigestsToManifest
(
JarFile
jar
,
int
hashes
)
private
boolean
closing
=
false
;
throws
IOException
,
GeneralSecurityException
{
private
ByteArrayOutputStream
footer
=
new
ByteArrayOutputStream
();
Manifest
input
=
jar
.
getManifest
();
private
OutputStream
tee
;
Manifest
output
=
new
Manifest
();
public
WholeFileSignerOutputStream
(
OutputStream
out
,
OutputStream
tee
)
{
Attributes
main
=
output
.
getMainAttributes
();
super
(
out
);
if
(
input
!=
null
)
{
this
.
tee
=
tee
;
main
.
putAll
(
input
.
getMainAttributes
());
}
}
else
{
public
void
notifyClosing
()
{
main
.
putValue
(
"Manifest-Version"
,
"1.0"
);
closing
=
true
;
main
.
putValue
(
"Created-By"
,
"1.0 (Android SignApk)"
);
}
}
public
void
finish
()
throws
IOException
{
MessageDigest
md_sha1
=
null
;
closing
=
false
;
MessageDigest
md_sha256
=
null
;
byte
[]
data
=
footer
.
toByteArray
();
if
((
hashes
&
USE_SHA1
)
!=
0
)
{
if
(
data
.
length
<
2
)
md_sha1
=
MessageDigest
.
getInstance
(
"SHA1"
);
throw
new
IOException
(
"Less than two bytes written to footer"
);
}
write
(
data
,
0
,
data
.
length
-
2
);
if
((
hashes
&
USE_SHA256
)
!=
0
)
{
}
md_sha256
=
MessageDigest
.
getInstance
(
"SHA256"
);
public
byte
[]
getTail
()
{
}
return
footer
.
toByteArray
();
byte
[]
buffer
=
new
byte
[
4096
];
int
num
;
// We sort the input entries by name, and add them to the
// output manifest in sorted order. We expect that the output
// map will be deterministic.
TreeMap
<
String
,
JarEntry
>
byName
=
new
TreeMap
<
String
,
JarEntry
>();
for
(
Enumeration
<
JarEntry
>
e
=
jar
.
entries
();
e
.
hasMoreElements
();
)
{
JarEntry
entry
=
e
.
nextElement
();
byName
.
put
(
entry
.
getName
(),
entry
);
}
for
(
JarEntry
entry:
byName
.
values
())
{
String
name
=
entry
.
getName
();
if
(!
entry
.
isDirectory
()
&&
(
stripPattern
==
null
||
!
stripPattern
.
matcher
(
name
).
matches
()))
{
InputStream
data
=
jar
.
getInputStream
(
entry
);
while
((
num
=
data
.
read
(
buffer
))
>
0
)
{
if
(
md_sha1
!=
null
)
md_sha1
.
update
(
buffer
,
0
,
num
);
if
(
md_sha256
!=
null
)
md_sha256
.
update
(
buffer
,
0
,
num
);
}
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
=
((
Attributes
.
Name
)
key
).
toString
().
toLowerCase
(
Locale
.
US
);
if
(
attributeNameLowerCase
.
endsWith
(
"-digest"
))
{
i
.
remove
();
}
}
// Add SHA-1 digest if requested
if
(
md_sha1
!=
null
)
{
attr
.
putValue
(
"SHA1-Digest"
,
new
String
(
Base64
.
encode
(
md_sha1
.
digest
()),
"ASCII"
));
}
// Add SHA-256 digest if requested
if
(
md_sha256
!=
null
)
{
attr
.
putValue
(
"SHA-256-Digest"
,
new
String
(
Base64
.
encode
(
md_sha256
.
digest
()),
"ASCII"
));
}
output
.
getEntries
().
put
(
name
,
attr
);
}
}
return
output
;
}
}
@Override
public
void
write
(
byte
[]
b
)
throws
IOException
{
private
static
void
copyFiles
(
Manifest
manifest
,
write
(
b
,
0
,
b
.
length
);
JarFile
in
,
JarOutputStream
out
,
long
timestamp
)
throws
IOException
{
byte
[]
buffer
=
new
byte
[
4096
];
int
num
;
Map
<
String
,
Attributes
>
entries
=
manifest
.
getEntries
();
ArrayList
<
String
>
names
=
new
ArrayList
<
String
>(
entries
.
keySet
());
Collections
.
sort
(
names
);
for
(
String
name
:
names
)
{
JarEntry
inEntry
=
in
.
getJarEntry
(
name
);
JarEntry
outEntry
=
null
;
if
(
inEntry
.
getMethod
()
==
JarEntry
.
STORED
)
{
// Preserve the STORED method of the input entry.
outEntry
=
new
JarEntry
(
inEntry
);
}
else
{
// Create a new entry so that the compressed len is recomputed.
outEntry
=
new
JarEntry
(
name
);
}
outEntry
.
setTime
(
timestamp
);
out
.
putNextEntry
(
outEntry
);
InputStream
data
=
in
.
getInputStream
(
inEntry
);
while
((
num
=
data
.
read
(
buffer
))
>
0
)
{
out
.
write
(
buffer
,
0
,
num
);
}
out
.
flush
();
}
}
}
@Override
public
void
write
(
byte
[]
b
,
int
off
,
int
len
)
throws
IOException
{
private
static
void
signWholeFile
(
JarFile
inputJar
,
InputStream
publicKeyFile
,
if
(
closing
)
{
X509Certificate
publicKey
,
PrivateKey
privateKey
,
// if the jar is about to close, save the footer that will be written
OutputStream
outputStream
)
throws
Exception
{
footer
.
write
(
b
,
off
,
len
);
CMSSigner
cmsOut
=
new
CMSSigner
(
inputJar
,
publicKeyFile
,
publicKey
,
privateKey
,
outputStream
);
ByteArrayOutputStream
temp
=
new
ByteArrayOutputStream
();
// put a readable message and a null char at the start of the
// archive comment, so that tools that display the comment
// (hopefully) show something sensible.
// TODO: anything more useful we can put in this message?
byte
[]
message
=
"signed by SignApk"
.
getBytes
(
"UTF-8"
);
temp
.
write
(
message
);
temp
.
write
(
0
);
cmsOut
.
writeSignatureBlock
(
temp
);
byte
[]
zipData
=
cmsOut
.
getSigner
().
getTail
();
// 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.
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
)
{
throw
new
IllegalArgumentException
(
"signature is too big for ZIP file comment"
);
}
}
else
{
// signature starts this many bytes from the end of the file
// write to both output streams. out is the CMSTypedData signer and tee is the file.
int
signature_start
=
total_size
-
message
.
length
-
1
;
out
.
write
(
b
,
off
,
len
);
temp
.
write
(
signature_start
&
0xff
);
tee
.
write
(
b
,
off
,
len
);
temp
.
write
((
signature_start
>>
8
)
&
0xff
);
// Why the 0xff bytes? In a zip file with no archive comment,
// 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
);
}
}
}
outputStream
.
write
(
total_size
&
0xff
);
outputStream
.
write
((
total_size
>>
8
)
&
0xff
);
temp
.
writeTo
(
outputStream
);
}
}
@Override
public
void
write
(
int
b
)
throws
IOException
{
private
static
class
CMSSigner
implements
CMSTypedData
{
if
(
closing
)
{
private
JarFile
inputJar
;
// if the jar is about to close, save the footer that will be written
private
InputStream
publicKeyFile
;
footer
.
write
(
b
);
private
X509Certificate
publicKey
;
private
PrivateKey
privateKey
;
private
OutputStream
outputStream
;
private
final
ASN1ObjectIdentifier
type
;
private
GeneralUtils
.
WholeFileSignerOutputStream
signer
;
public
CMSSigner
(
JarFile
inputJar
,
InputStream
publicKeyFile
,
X509Certificate
publicKey
,
PrivateKey
privateKey
,
OutputStream
outputStream
)
{
this
.
inputJar
=
inputJar
;
this
.
publicKeyFile
=
publicKeyFile
;
this
.
publicKey
=
publicKey
;
this
.
privateKey
=
privateKey
;
this
.
outputStream
=
outputStream
;
this
.
type
=
new
ASN1ObjectIdentifier
(
CMSObjectIdentifiers
.
data
.
getId
());
}
public
Object
getContent
()
{
// Not supported, but still don't crash or return null
return
1
;
}
public
ASN1ObjectIdentifier
getContentType
()
{
return
type
;
}
public
void
write
(
OutputStream
out
)
throws
IOException
{
try
{
signer
=
new
GeneralUtils
.
WholeFileSignerOutputStream
(
out
,
outputStream
);
JarOutputStream
outputJar
=
new
JarOutputStream
(
signer
);
int
hash
=
getDigestAlgorithm
(
publicKey
);
// Assume the certificate is valid for at least an hour.
long
timestamp
=
publicKey
.
getNotBefore
().
getTime
()
+
3600L
*
1000
;
Manifest
manifest
=
addDigestsToManifest
(
inputJar
,
hash
);
copyFiles
(
manifest
,
inputJar
,
outputJar
,
timestamp
);
// Don't add Otacert, it's not an OTA
// addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
GeneralUtils
.
signFile
(
manifest
,
publicKey
,
privateKey
,
outputJar
);
signer
.
notifyClosing
();
outputJar
.
close
();
signer
.
finish
();
}
catch
(
Exception
e
)
{
throw
new
IOException
(
e
);
}
}
}
else
{
public
void
writeSignatureBlock
(
ByteArrayOutputStream
temp
)
// write to both output streams. out is the CMSTypedData signer and tee is the file.
throws
IOException
,
out
.
write
(
b
);
CertificateEncodingException
,
tee
.
write
(
b
);
OperatorCreationException
,
CMSException
{
GeneralUtils
.
writeSignatureBlock
(
this
,
publicKey
,
privateKey
,
temp
);
}
public
GeneralUtils
.
WholeFileSignerOutputStream
getSigner
()
{
return
signer
;
}
}
}
}
}
}
private
static
class
CMSSigner
implements
CMSTypedData
{
// This class host functions that consumes inputstreams
private
JarMap
inputJar
;
// Uses JarMap (virtual random access JarFile in memory)
private
InputStream
publicKeyFile
;
public
static
class
StreamUtils
{
private
X509Certificate
publicKey
;
private
PrivateKey
privateKey
;
/**
private
OutputStream
outputStream
;
* Add the hash(es) of every file to the manifest, creating it if
private
final
ASN1ObjectIdentifier
type
;
* necessary.
private
WholeFileSignerOutputStream
signer
;
*/
public
CMSSigner
(
JarMap
inputJar
,
InputStream
publicKeyFile
,
private
static
Manifest
addDigestsToManifest
(
JarMap
jar
,
int
hashes
)
X509Certificate
publicKey
,
PrivateKey
privateKey
,
throws
IOException
,
GeneralSecurityException
{
OutputStream
outputStream
)
{
Manifest
input
=
jar
.
getManifest
();
this
.
inputJar
=
inputJar
;
Manifest
output
=
new
Manifest
();
this
.
publicKeyFile
=
publicKeyFile
;
Attributes
main
=
output
.
getMainAttributes
();
this
.
publicKey
=
publicKey
;
if
(
input
!=
null
)
{
this
.
privateKey
=
privateKey
;
main
.
putAll
(
input
.
getMainAttributes
());
this
.
outputStream
=
outputStream
;
}
else
{
this
.
type
=
new
ASN1ObjectIdentifier
(
CMSObjectIdentifiers
.
data
.
getId
());
main
.
putValue
(
"Manifest-Version"
,
"1.0"
);
}
main
.
putValue
(
"Created-By"
,
"1.0 (Android SignApk)"
);
public
Object
getContent
()
{
// Not supported, but still don't crash or return null
return
1
;
}
public
ASN1ObjectIdentifier
getContentType
()
{
return
type
;
}
public
void
write
(
OutputStream
out
)
throws
IOException
{
try
{
signer
=
new
WholeFileSignerOutputStream
(
out
,
outputStream
);
JarOutputStream
outputJar
=
new
JarOutputStream
(
signer
);
int
hash
=
getDigestAlgorithm
(
publicKey
);
// Assume the certificate is valid for at least an hour.
long
timestamp
=
publicKey
.
getNotBefore
().
getTime
()
+
3600L
*
1000
;
Manifest
manifest
=
addDigestsToManifest
(
inputJar
,
hash
);
copyFiles
(
manifest
,
inputJar
,
outputJar
,
timestamp
);
// Don't add Otacert, it's not an OTA
// addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
signFile
(
manifest
,
inputJar
,
publicKey
,
privateKey
,
outputJar
);
signer
.
notifyClosing
();
outputJar
.
close
();
signer
.
finish
();
}
}
catch
(
Exception
e
)
{
MessageDigest
md_sha1
=
null
;
throw
new
IOException
(
e
);
MessageDigest
md_sha256
=
null
;
if
((
hashes
&
USE_SHA1
)
!=
0
)
{
md_sha1
=
MessageDigest
.
getInstance
(
"SHA1"
);
}
}
if
((
hashes
&
USE_SHA256
)
!=
0
)
{
md_sha256
=
MessageDigest
.
getInstance
(
"SHA256"
);
}
// We sort the input entries by name, and add them to the
// output manifest in sorted order. We expect that the output
// map will be deterministic.
/* JarMap is a TreeMap, so it's already sorted */
for
(
String
name
:
jar
.
keySet
())
{
JarEntry
entry
=
jar
.
getJarEntry
(
name
);
if
(!
entry
.
isDirectory
()
&&
(
stripPattern
==
null
||
!
stripPattern
.
matcher
(
name
).
matches
()))
{
byte
[]
buffer
=
jar
.
getStream
(
name
).
toByteArray
();
if
(
md_sha1
!=
null
)
md_sha1
.
update
(
buffer
,
0
,
buffer
.
length
);
if
(
md_sha256
!=
null
)
md_sha256
.
update
(
buffer
,
0
,
buffer
.
length
);
Attributes
attr
=
null
;
if
(
input
!=
null
)
attr
=
input
.
getAttributes
(
name
);
attr
=
attr
!=
null
?
new
Attributes
(
attr
)
:
new
Attributes
();
if
(
md_sha1
!=
null
)
{
attr
.
putValue
(
"SHA1-Digest"
,
new
String
(
Base64
.
encode
(
md_sha1
.
digest
()),
"ASCII"
));
}
if
(
md_sha256
!=
null
)
{
attr
.
putValue
(
"SHA-256-Digest"
,
new
String
(
Base64
.
encode
(
md_sha256
.
digest
()),
"ASCII"
));
}
output
.
getEntries
().
put
(
name
,
attr
);
}
}
return
output
;
}
}
public
void
writeSignatureBlock
(
ByteArrayOutputStream
temp
)
throws
IOException
,
CertificateEncodingException
,
OperatorCreationException
,
CMSException
{
ZipUtils
.
writeSignatureBlock
(
this
,
publicKey
,
privateKey
,
temp
);
}
public
WholeFileSignerOutputStream
getSigner
()
{
return
signer
;
}
}
private
static
void
signWholeFile
(
JarMap
inputJar
,
InputStream
publicKeyFile
,
/**
X509Certificate
publicKey
,
PrivateKey
privateKey
,
* Copy all the files in a manifest from input to output. We set
OutputStream
outputStream
)
throws
Exception
{
* the modification times in the output to a fixed time, so as to
CMSSigner
cmsOut
=
new
CMSSigner
(
inputJar
,
publicKeyFile
,
* reduce variation in the output file and make incremental OTAs
publicKey
,
privateKey
,
outputStream
);
* more efficient.
ByteArrayOutputStream
temp
=
new
ByteArrayOutputStream
();
*/
// put a readable message and a null char at the start of the
private
static
void
copyFiles
(
Manifest
manifest
,
// archive comment, so that tools that display the comment
JarMap
in
,
JarOutputStream
out
,
long
timestamp
)
throws
IOException
{
// (hopefully) show something sensible.
Map
<
String
,
Attributes
>
entries
=
manifest
.
getEntries
();
// TODO: anything more useful we can put in this message?
ArrayList
<
String
>
names
=
new
ArrayList
<>(
entries
.
keySet
());
byte
[]
message
=
"signed by SignApk"
.
getBytes
(
"UTF-8"
);
Collections
.
sort
(
names
);
temp
.
write
(
message
);
for
(
String
name
:
names
)
{
temp
.
write
(
0
);
JarEntry
inEntry
=
in
.
getJarEntry
(
name
);
cmsOut
.
writeSignatureBlock
(
temp
);
JarEntry
outEntry
;
byte
[]
zipData
=
cmsOut
.
getSigner
().
getTail
();
if
(
inEntry
.
getMethod
()
==
JarEntry
.
STORED
)
{
// For a zip with no archive comment, the
// Preserve the STORED method of the input entry.
// end-of-central-directory record will be 22 bytes long, so
outEntry
=
new
JarEntry
(
inEntry
);
// we expect to find the EOCD marker 22 bytes from the end.
}
else
{
if
(
zipData
[
zipData
.
length
-
22
]
!=
0x50
||
// Create a new entry so that the compressed len is recomputed.
zipData
[
zipData
.
length
-
21
]
!=
0x4b
||
outEntry
=
new
JarEntry
(
name
);
zipData
[
zipData
.
length
-
20
]
!=
0x05
||
}
zipData
[
zipData
.
length
-
19
]
!=
0x06
)
{
outEntry
.
setTime
(
timestamp
);
throw
new
IllegalArgumentException
(
"zip data already has an archive comment"
);
out
.
putNextEntry
(
outEntry
);
in
.
getStream
(
name
).
writeTo
(
out
);
out
.
flush
();
}
}
}
int
total_size
=
temp
.
size
()
+
6
;
if
(
total_size
>
0xffff
)
{
private
static
void
signWholeFile
(
JarMap
inputJar
,
InputStream
publicKeyFile
,
throw
new
IllegalArgumentException
(
"signature is too big for ZIP file comment"
);
X509Certificate
publicKey
,
PrivateKey
privateKey
,
OutputStream
outputStream
)
throws
Exception
{
CMSSigner
cmsOut
=
new
CMSSigner
(
inputJar
,
publicKeyFile
,
publicKey
,
privateKey
,
outputStream
);
ByteArrayOutputStream
temp
=
new
ByteArrayOutputStream
();
// put a readable message and a null char at the start of the
// archive comment, so that tools that display the comment
// (hopefully) show something sensible.
// TODO: anything more useful we can put in this message?
byte
[]
message
=
"signed by SignApk"
.
getBytes
(
"UTF-8"
);
temp
.
write
(
message
);
temp
.
write
(
0
);
cmsOut
.
writeSignatureBlock
(
temp
);
byte
[]
zipData
=
cmsOut
.
getSigner
().
getTail
();
// 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.
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
)
{
throw
new
IllegalArgumentException
(
"signature is too big for ZIP file comment"
);
}
// signature starts this many bytes from the end of the file
int
signature_start
=
total_size
-
message
.
length
-
1
;
temp
.
write
(
signature_start
&
0xff
);
temp
.
write
((
signature_start
>>
8
)
&
0xff
);
// Why the 0xff bytes? In a zip file with no archive comment,
// 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
);
}
}
outputStream
.
write
(
total_size
&
0xff
);
outputStream
.
write
((
total_size
>>
8
)
&
0xff
);
temp
.
writeTo
(
outputStream
);
}
}
// signature starts this many bytes from the end of the file
int
signature_start
=
total_size
-
message
.
length
-
1
;
public
static
class
JarMap
extends
TreeMap
<
String
,
Pair
<
JarEntry
,
ByteArrayOutputStream
>
>
{
temp
.
write
(
signature_start
&
0xff
);
temp
.
write
((
signature_start
>>
8
)
&
0xff
);
private
Manifest
manifest
;
// Why the 0xff bytes? In a zip file with no archive comment,
// bytes [-6:-2] of the file are the little-endian offset from
public
JarMap
(
JarInputStream
in
)
throws
IOException
{
// the start of the file to the central directory. So for the
super
();
// two high bytes to be 0xff 0xff, the archive would have to
manifest
=
in
.
getManifest
();
// be nearly 4GB in size. So it's unlikely that a real
byte
[]
buffer
=
new
byte
[
4096
];
// commentless archive would have 0xffs here, and lets us tell
int
num
;
// an old signed archive from a new one.
JarEntry
entry
;
temp
.
write
(
0xff
);
while
((
entry
=
in
.
getNextJarEntry
())
!=
null
)
{
temp
.
write
(
0xff
);
ByteArrayOutputStream
stream
=
new
ByteArrayOutputStream
();
temp
.
write
(
total_size
&
0xff
);
while
((
num
=
in
.
read
(
buffer
))
>
0
)
{
temp
.
write
((
total_size
>>
8
)
&
0xff
);
stream
.
write
(
buffer
,
0
,
num
);
temp
.
flush
();
}
// Signature verification checks that the EOCD header is the
put
(
entry
.
getName
(),
entry
,
stream
);
// last such sequence in the file (to avoid minzip finding a
}
// fake EOCD appended after the signature in its scan). The
in
.
close
();
// odds of producing this sequence by chance are very low, but
}
// let's catch it here if it does.
byte
[]
b
=
temp
.
toByteArray
();
public
JarEntry
getJarEntry
(
String
name
)
{
for
(
int
i
=
0
;
i
<
b
.
length
-
3
;
++
i
)
{
return
get
(
name
).
first
;
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
);
public
ByteArrayOutputStream
getStream
(
String
name
)
{
return
get
(
name
).
second
;
}
public
void
put
(
String
name
,
JarEntry
entry
,
ByteArrayOutputStream
stream
)
{
put
(
name
,
new
Pair
<>(
entry
,
stream
));
}
public
Manifest
getManifest
()
{
return
manifest
;
}
public
Enumeration
<
JarEntry
>
entries
()
{
Iterator
<
Entry
<
String
,
Pair
<
JarEntry
,
ByteArrayOutputStream
>
>>
i
=
entrySet
().
iterator
();
ArrayList
<
JarEntry
>
list
=
new
ArrayList
<>();
while
(
i
.
hasNext
())
list
.
add
(
i
.
next
().
getValue
().
first
);
return
Collections
.
enumeration
(
list
);
}
}
}
}
outputStream
.
write
(
total_size
&
0xff
);
outputStream
.
write
((
total_size
>>
8
)
&
0xff
);
temp
.
writeTo
(
outputStream
);
}
private
static
void
signFile
(
Manifest
manifest
,
JarMap
inputJar
,
private
static
class
CMSSigner
implements
CMSTypedData
{
X509Certificate
publicKey
,
PrivateKey
privateKey
,
private
JarMap
inputJar
;
JarOutputStream
outputJar
)
private
InputStream
publicKeyFile
;
throws
Exception
{
private
X509Certificate
publicKey
;
// Assume the certificate is valid for at least an hour.
private
PrivateKey
privateKey
;
long
timestamp
=
publicKey
.
getNotBefore
().
getTime
()
+
3600L
*
1000
;
private
OutputStream
outputStream
;
// MANIFEST.MF
private
final
ASN1ObjectIdentifier
type
;
JarEntry
je
=
new
JarEntry
(
JarFile
.
MANIFEST_NAME
);
private
GeneralUtils
.
WholeFileSignerOutputStream
signer
;
je
.
setTime
(
timestamp
);
public
CMSSigner
(
JarMap
inputJar
,
InputStream
publicKeyFile
,
outputJar
.
putNextEntry
(
je
);
X509Certificate
publicKey
,
PrivateKey
privateKey
,
manifest
.
write
(
outputJar
);
OutputStream
outputStream
)
{
// CERT.SF / CERT#.SF
this
.
inputJar
=
inputJar
;
je
=
new
JarEntry
(
CERT_SF_NAME
);
this
.
publicKeyFile
=
publicKeyFile
;
je
.
setTime
(
timestamp
);
this
.
publicKey
=
publicKey
;
outputJar
.
putNextEntry
(
je
);
this
.
privateKey
=
privateKey
;
ByteArrayOutputStream
baos
=
new
ByteArrayOutputStream
();
this
.
outputStream
=
outputStream
;
writeSignatureFile
(
manifest
,
baos
,
getDigestAlgorithm
(
publicKey
));
this
.
type
=
new
ASN1ObjectIdentifier
(
CMSObjectIdentifiers
.
data
.
getId
());
byte
[]
signedData
=
baos
.
toByteArray
();
}
outputJar
.
write
(
signedData
);
public
Object
getContent
()
{
// CERT.{DSA,EC,RSA} / CERT#.{DSA,EC,RSA}
// Not supported, but still don't crash or return null
je
=
new
JarEntry
((
String
.
format
(
CERT_SIG_NAME
,
privateKey
.
getAlgorithm
())));
return
1
;
je
.
setTime
(
timestamp
);
}
outputJar
.
putNextEntry
(
je
);
public
ASN1ObjectIdentifier
getContentType
()
{
writeSignatureBlock
(
new
CMSProcessableByteArray
(
signedData
),
return
type
;
publicKey
,
privateKey
,
outputJar
);
}
public
void
write
(
OutputStream
out
)
throws
IOException
{
try
{
signer
=
new
GeneralUtils
.
WholeFileSignerOutputStream
(
out
,
outputStream
);
JarOutputStream
outputJar
=
new
JarOutputStream
(
signer
);
int
hash
=
FileUtils
.
getDigestAlgorithm
(
publicKey
);
// Assume the certificate is valid for at least an hour.
long
timestamp
=
publicKey
.
getNotBefore
().
getTime
()
+
3600L
*
1000
;
Manifest
manifest
=
addDigestsToManifest
(
inputJar
,
hash
);
copyFiles
(
manifest
,
inputJar
,
outputJar
,
timestamp
);
// Don't add Otacert, it's not an OTA
// addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
GeneralUtils
.
signFile
(
manifest
,
publicKey
,
privateKey
,
outputJar
);
signer
.
notifyClosing
();
outputJar
.
close
();
signer
.
finish
();
}
catch
(
Exception
e
)
{
throw
new
IOException
(
e
);
}
}
public
void
writeSignatureBlock
(
ByteArrayOutputStream
temp
)
throws
IOException
,
CertificateEncodingException
,
OperatorCreationException
,
CMSException
{
GeneralUtils
.
writeSignatureBlock
(
this
,
publicKey
,
privateKey
,
temp
);
}
public
GeneralUtils
.
WholeFileSignerOutputStream
getSigner
()
{
return
signer
;
}
}
}
}
}
}
app/src/main/jni/jni_glue.c
View file @
d1c939f4
...
@@ -4,10 +4,15 @@
...
@@ -4,10 +4,15 @@
#include <jni.h>
#include <jni.h>
#include <stdlib.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "zipadjust.h"
#include "zipadjust.h"
JNIEXPORT
jbyteArray
JNICALL
JNIEXPORT
jbyteArray
JNICALL
Java_com_topjohnwu_magisk_utils_ZipUtils_zipAdjust
(
JNIEnv
*
env
,
jclass
type
,
jbyteArray
jbytes
,
jint
size
)
{
Java_com_topjohnwu_magisk_utils_ZipUtils_zipAdjust___3BI
(
JNIEnv
*
env
,
jclass
type
,
jbyteArray
jbytes
,
jint
size
)
{
fin
=
(
*
env
)
->
GetPrimitiveArrayCritical
(
env
,
jbytes
,
NULL
);
fin
=
(
*
env
)
->
GetPrimitiveArrayCritical
(
env
,
jbytes
,
NULL
);
insize
=
(
size_t
)
size
;
insize
=
(
size_t
)
size
;
...
@@ -21,3 +26,37 @@ Java_com_topjohnwu_magisk_utils_ZipUtils_zipAdjust(JNIEnv *env, jclass type, jby
...
@@ -21,3 +26,37 @@ Java_com_topjohnwu_magisk_utils_ZipUtils_zipAdjust(JNIEnv *env, jclass type, jby
return
ret
;
return
ret
;
}
}
JNIEXPORT
void
JNICALL
Java_com_topjohnwu_magisk_utils_ZipUtils_zipAdjust__Ljava_lang_String_2
(
JNIEnv
*
env
,
jclass
type
,
jstring
name
)
{
const
char
*
filename
=
(
*
env
)
->
GetStringUTFChars
(
env
,
name
,
NULL
);
int
fd
=
open
(
filename
,
O_RDONLY
);
if
(
fd
<
0
)
return
;
// Load the file to memory
insize
=
lseek
(
fd
,
0
,
SEEK_END
);
lseek
(
fd
,
0
,
SEEK_SET
);
fin
=
malloc
(
insize
);
read
(
fd
,
fin
,
insize
);
zipadjust
(
0
);
close
(
fd
);
// Open file for output
fd
=
open
(
filename
,
O_WRONLY
|
O_TRUNC
);
if
(
fd
<
0
)
return
;
(
*
env
)
->
ReleaseStringUTFChars
(
env
,
name
,
filename
);
// Write back to file
lseek
(
fd
,
0
,
SEEK_SET
);
write
(
fd
,
fout
,
outsize
);
close
(
fd
);
free
(
fin
);
free
(
fout
);
}
app/src/main/jni/zipadjust.c
View file @
d1c939f4
#include <stdlib.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdio.h>
#include <zlib.h>
#include <zlib.h>
#include <android/log.h>
#include "zipadjust.h"
#include "zipadjust.h"
#define LOG_TAG "zipadjust"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
size_t
insize
=
0
,
outsize
=
0
,
alloc
=
0
;
size_t
insize
=
0
,
outsize
=
0
,
alloc
=
0
;
unsigned
char
*
fin
=
NULL
,
*
fout
=
NULL
;
unsigned
char
*
fin
=
NULL
,
*
fout
=
NULL
;
...
...
app/src/main/jni/zipadjust.h
View file @
d1c939f4
#ifndef MAGISKMANAGER_ZIPADJUST_H_H
#ifndef MAGISKMANAGER_ZIPADJUST_H_H
#define MAGISKMANAGER_ZIPADJUST_H_H
#define MAGISKMANAGER_ZIPADJUST_H_H
#include <android/log.h>
int
zipadjust
(
int
decompress
);
int
zipadjust
(
int
decompress
);
extern
size_t
insize
,
outsize
,
alloc
;
extern
size_t
insize
,
outsize
,
alloc
;
extern
unsigned
char
*
fin
,
*
fout
;
extern
unsigned
char
*
fin
,
*
fout
;
#define LOG_TAG "zipadjust"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#endif //MAGISKMANAGER_ZIPADJUST_H_H
#endif //MAGISKMANAGER_ZIPADJUST_H_H
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