+++ /dev/null
-#include <host/pseudolocalize.h>
-
-using namespace std;
-
-static const char*
-pseudolocalize_char(char c)
-{
- switch (c) {
- case 'a': return "\xc4\x83";
- case 'b': return "\xcf\x84";
- case 'c': return "\xc4\x8b";
- case 'd': return "\xc4\x8f";
- case 'e': return "\xc4\x99";
- case 'f': return "\xc6\x92";
- case 'g': return "\xc4\x9d";
- case 'h': return "\xd1\x9b";
- case 'i': return "\xcf\x8a";
- case 'j': return "\xc4\xb5";
- case 'k': return "\xc4\xb8";
- case 'l': return "\xc4\xba";
- case 'm': return "\xe1\xb8\xbf";
- case 'n': return "\xd0\xb8";
- case 'o': return "\xcf\x8c";
- case 'p': return "\xcf\x81";
- case 'q': return "\x51";
- case 'r': return "\xd2\x91";
- case 's': return "\xc5\xa1";
- case 't': return "\xd1\x82";
- case 'u': return "\xce\xb0";
- case 'v': return "\x56";
- case 'w': return "\xe1\xba\x85";
- case 'x': return "\xd1\x85";
- case 'y': return "\xe1\xbb\xb3";
- case 'z': return "\xc5\xba";
- case 'A': return "\xc3\x85";
- case 'B': return "\xce\xb2";
- case 'C': return "\xc4\x88";
- case 'D': return "\xc4\x90";
- case 'E': return "\xd0\x84";
- case 'F': return "\xce\x93";
- case 'G': return "\xc4\x9e";
- case 'H': return "\xc4\xa6";
- case 'I': return "\xd0\x87";
- case 'J': return "\xc4\xb5";
- case 'K': return "\xc4\xb6";
- case 'L': return "\xc5\x81";
- case 'M': return "\xe1\xb8\xbe";
- case 'N': return "\xc5\x83";
- case 'O': return "\xce\x98";
- case 'P': return "\xcf\x81";
- case 'Q': return "\x71";
- case 'R': return "\xd0\xaf";
- case 'S': return "\xc8\x98";
- case 'T': return "\xc5\xa6";
- case 'U': return "\xc5\xa8";
- case 'V': return "\xce\xbd";
- case 'W': return "\xe1\xba\x84";
- case 'X': return "\xc3\x97";
- case 'Y': return "\xc2\xa5";
- case 'Z': return "\xc5\xbd";
- default: return NULL;
- }
-}
-
-/**
- * Converts characters so they look like they've been localized.
- *
- * Note: This leaves escape sequences untouched so they can later be
- * processed by ResTable::collectString in the normal way.
- */
-string
-pseudolocalize_string(const string& source)
-{
- const char* s = source.c_str();
- string result;
- const size_t I = source.length();
- for (size_t i=0; i<I; i++) {
- char c = s[i];
- if (c == '\\') {
- if (i<I-1) {
- result += '\\';
- i++;
- c = s[i];
- switch (c) {
- case 'u':
- // this one takes up 5 chars
- result += string(s+i, 5);
- i += 4;
- break;
- case 't':
- case 'n':
- case '#':
- case '@':
- case '?':
- case '"':
- case '\'':
- case '\\':
- default:
- result += c;
- break;
- }
- } else {
- result += c;
- }
- } else {
- const char* p = pseudolocalize_char(c);
- if (p != NULL) {
- result += p;
- } else {
- result += c;
- }
- }
- }
-
- //printf("result=\'%s\'\n", result.c_str());
- return result;
-}
-
-
import javax.crypto.spec.PBEKeySpec;
/**
- * Command line tool to sign JAR files (including APKs and OTA updates) in
- * a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
+ * HISTORICAL NOTE:
+ *
+ * Prior to the keylimepie release, SignApk ignored the signature
+ * algorithm specified in the certificate and always used SHA1withRSA.
+ *
+ * Starting with keylimepie, we support SHA256withRSA, and use the
+ * signature algorithm in the certificate to select which to use
+ * (SHA256withRSA or SHA1withRSA).
+ *
+ * Because there are old keys still in use whose certificate actually
+ * says "MD5withRSA", we treat these as though they say "SHA1withRSA"
+ * for compatibility with older releases. This can be changed by
+ * altering the getAlgorithm() function below.
+ */
+
+
+/**
+ * Command line tool to sign JAR files (including APKs and OTA
+ * updates) in a way compatible with the mincrypt verifier, using RSA
+ * keys and SHA1 or SHA-256.
*/
class SignApk {
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
private static Provider sBouncyCastleProvider;
+ // bitmasks for which hash algorithms we need the manifest to include.
+ private static final int USE_SHA1 = 1;
+ private static final int USE_SHA256 = 2;
+
+ /**
+ * Return one of USE_SHA1 or USE_SHA256 according to the signature
+ * algorithm specified in the cert.
+ */
+ private static int getAlgorithm(X509Certificate cert) {
+ String sigAlg = cert.getSigAlgName();
+ if ("SHA1withRSA".equals(sigAlg) ||
+ "MD5withRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above.
+ return USE_SHA1;
+ } else if ("SHA256withRSA".equals(sigAlg)) {
+ return USE_SHA256;
+ } else {
+ throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
+ "\" in cert [" + cert.getSubjectDN());
+ }
+ }
+
// Files matching this pattern are not copied to the output.
private static Pattern stripPattern =
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA)|com/android/otacert))|(" +
}
}
- /** Add the SHA1 of every file to the manifest, creating it if necessary. */
- private static Manifest addDigestsToManifest(JarFile jar)
+ /**
+ * Add the hash(es) of every file to the manifest, creating it if
+ * necessary.
+ */
+ private static Manifest addDigestsToManifest(JarFile jar, int hashes)
throws IOException, GeneralSecurityException {
Manifest input = jar.getManifest();
Manifest output = new Manifest();
main.putValue("Created-By", "1.0 (Android SignApk)");
}
- MessageDigest md = MessageDigest.getInstance("SHA1");
+ MessageDigest md_sha1 = null;
+ MessageDigest md_sha256 = null;
+ if ((hashes & USE_SHA1) != 0) {
+ md_sha1 = MessageDigest.getInstance("SHA1");
+ }
+ if ((hashes & USE_SHA256) != 0) {
+ md_sha256 = MessageDigest.getInstance("SHA256");
+ }
+
byte[] buffer = new byte[4096];
int num;
(stripPattern == null || !stripPattern.matcher(name).matches())) {
InputStream data = jar.getInputStream(entry);
while ((num = data.read(buffer)) > 0) {
- md.update(buffer, 0, num);
+ 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();
- attr.putValue("SHA1-Digest",
- new String(Base64.encode(md.digest()), "ASCII"));
+ 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);
}
}
private static void addOtacert(JarOutputStream outputJar,
File publicKeyFile,
long timestamp,
- Manifest manifest)
+ Manifest manifest,
+ int hash)
throws IOException, GeneralSecurityException {
- MessageDigest md = MessageDigest.getInstance("SHA1");
+ MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
JarEntry je = new JarEntry(OTACERT_NAME);
je.setTime(timestamp);
input.close();
Attributes attr = new Attributes();
- attr.putValue("SHA1-Digest",
+ attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
new String(Base64.encode(md.digest()), "ASCII"));
manifest.getEntries().put(OTACERT_NAME, attr);
}
}
/** Write a .SF file with a digest of the specified manifest. */
- private static void writeSignatureFile(Manifest manifest, OutputStream out)
+ private static void writeSignatureFile(Manifest manifest, OutputStream out,
+ int hash)
throws IOException, GeneralSecurityException {
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
- MessageDigest md = MessageDigest.getInstance("SHA1");
+ MessageDigest md = MessageDigest.getInstance(
+ hash == USE_SHA256 ? "SHA256" : "SHA1");
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
// Digest of the entire manifest
manifest.write(print);
print.flush();
- main.putValue("SHA1-Digest-Manifest",
+ main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
new String(Base64.encode(md.digest()), "ASCII"));
Map<String, Attributes> entries = manifest.getEntries();
print.flush();
Attributes sfAttr = new Attributes();
- sfAttr.putValue("SHA1-Digest",
+ sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
new String(Base64.encode(md.digest()), "ASCII"));
sf.getEntries().put(entry.getKey(), sfAttr);
}
JcaCertStore certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
- ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA")
+ ContentSigner signer = new JcaContentSignerBuilder(
+ getAlgorithm(publicKey) == USE_SHA256 ? "SHA256withRSA" : "SHA1withRSA")
.setProvider(sBouncyCastleProvider)
.build(privateKey);
gen.addSignerInfoGenerator(
.setProvider(sBouncyCastleProvider)
.build())
.setDirectSignature(true)
- .build(sha1Signer, publicKey));
+ .build(signer, publicKey));
gen.addCertificates(certs);
CMSSignedData sigData = gen.generate(data, false);
signer = new WholeFileSignerOutputStream(out, outputStream);
JarOutputStream outputJar = new JarOutputStream(signer);
- Manifest manifest = addDigestsToManifest(inputJar);
+ int hash = getAlgorithm(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);
+ addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
+
signFile(manifest, inputJar,
new X509Certificate[]{ publicKey },
new PrivateKey[]{ privateKey },
outputJar);
- // Assume the certificate is valid for at least an hour.
- long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
- addOtacert(outputJar, publicKeyFile, timestamp, manifest);
signer.notifyClosing();
outputJar.close();
// Assume the certificate is valid for at least an hour.
long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
- JarEntry je;
-
- // Everything else
- copyFiles(manifest, inputJar, outputJar, timestamp);
-
// MANIFEST.MF
- je = new JarEntry(JarFile.MANIFEST_NAME);
+ JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
je.setTime(timestamp);
outputJar.putNextEntry(je);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
- writeSignatureFile(manifest, baos);
+ writeSignatureFile(manifest, baos, getAlgorithm(publicKey[k]));
byte[] signedData = baos.toByteArray();
outputJar.write(signedData);
JarFile inputJar = null;
FileOutputStream outputFile = null;
+ int hashes = 0;
try {
File firstPublicKeyFile = new File(args[argstart+0]);
X509Certificate[] publicKey = new X509Certificate[numKeys];
- for (int i = 0; i < numKeys; ++i) {
- int argNum = argstart + i*2;
- publicKey[i] = readPublicKey(new File(args[argNum]));
+ try {
+ for (int i = 0; i < numKeys; ++i) {
+ int argNum = argstart + i*2;
+ publicKey[i] = readPublicKey(new File(args[argNum]));
+ hashes |= getAlgorithm(publicKey[i]);
+ }
+ } catch (IllegalArgumentException e) {
+ System.err.println(e);
+ System.exit(1);
}
// Set the ZIP file timestamp to the starting valid time
// (~0.1% on full OTA packages I tested).
outputJar.setLevel(9);
- signFile(addDigestsToManifest(inputJar), inputJar,
- publicKey, privateKey, outputJar);
+ Manifest manifest = addDigestsToManifest(inputJar, hashes);
+ copyFiles(manifest, inputJar, outputJar, timestamp);
+ signFile(manifest, inputJar, publicKey, privateKey, outputJar);
outputJar.close();
}
} catch (Exception e) {