merge in oc-release history after reset to master
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index cf6de35..06b5603 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -31,9 +31,11 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
+import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStore;
+import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
@@ -65,7 +67,7 @@
*/
public class ApkSignerTool {
- private static final String VERSION = "0.4";
+ private static final String VERSION = "0.5";
private static final String HELP_PAGE_GENERAL = "help.txt";
private static final String HELP_PAGE_SIGN = "help_sign.txt";
private static final String HELP_PAGE_VERIFY = "help_verify.txt";
@@ -621,20 +623,17 @@
}
// 2. Load the KeyStore
- char[] keystorePwd = null;
+ List<char[]> keystorePasswords = null;
if ("NONE".equals(keystoreFile)) {
ks.load(null);
} else {
String keystorePasswordSpec =
(this.keystorePasswordSpec != null)
? this.keystorePasswordSpec : PasswordRetriever.SPEC_STDIN;
- String keystorePwdString =
- passwordRetriever.getPassword(
+ keystorePasswords =
+ passwordRetriever.getPasswords(
keystorePasswordSpec, "Keystore password for " + name);
- keystorePwd = keystorePwdString.toCharArray();
- try (FileInputStream in = new FileInputStream(keystoreFile)) {
- ks.load(in, keystorePwd);
- }
+ loadKeyStoreFromFile(ks, keystoreFile, keystorePasswords);
}
// 3. Load the PrivateKey and cert chain from KeyStore
@@ -677,26 +676,24 @@
if (keyPasswordSpec != null) {
// Key password spec is explicitly specified. Use this spec to obtain the
// password and then load the key using that password.
- char[] keyPwd =
- passwordRetriever.getPassword(
+ List<char[]> keyPasswords =
+ passwordRetriever.getPasswords(
keyPasswordSpec,
- "Key \"" + keyAlias + "\" password for " + name)
- .toCharArray();
- entryKey = ks.getKey(keyAlias, keyPwd);
+ "Key \"" + keyAlias + "\" password for " + name);
+ entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords);
} else {
// Key password spec is not specified. This means we should assume that key
// password is the same as the keystore password and that, if this assumption is
// wrong, we should prompt for key password and retry loading the key using that
// password.
try {
- entryKey = ks.getKey(keyAlias, keystorePwd);
+ entryKey = getKeyStoreKey(ks, keyAlias, keystorePasswords);
} catch (UnrecoverableKeyException expected) {
- char[] keyPwd =
- passwordRetriever.getPassword(
+ List<char[]> keyPasswords =
+ passwordRetriever.getPasswords(
PasswordRetriever.SPEC_STDIN,
- "Key \"" + keyAlias + "\" password for " + name)
- .toCharArray();
- entryKey = ks.getKey(keyAlias, keyPwd);
+ "Key \"" + keyAlias + "\" password for " + name);
+ entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords);
}
}
@@ -728,6 +725,43 @@
}
}
+ private static void loadKeyStoreFromFile(KeyStore ks, String file, List<char[]> passwords)
+ throws Exception {
+ Exception lastFailure = null;
+ for (char[] password : passwords) {
+ try {
+ try (FileInputStream in = new FileInputStream(file)) {
+ ks.load(in, password);
+ }
+ return;
+ } catch (Exception e) {
+ lastFailure = e;
+ }
+ }
+ if (lastFailure == null) {
+ throw new RuntimeException("No keystore passwords");
+ } else {
+ throw lastFailure;
+ }
+ }
+
+ private static Key getKeyStoreKey(KeyStore ks, String keyAlias, List<char[]> passwords)
+ throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
+ UnrecoverableKeyException lastFailure = null;
+ for (char[] password : passwords) {
+ try {
+ return ks.getKey(keyAlias, password);
+ } catch (UnrecoverableKeyException e) {
+ lastFailure = e;
+ }
+ }
+ if (lastFailure == null) {
+ throw new RuntimeException("No key passwords");
+ } else {
+ throw lastFailure;
+ }
+ }
+
private void loadPrivateKeyAndCertsFromFiles(PasswordRetriever passwordRetriver)
throws Exception {
if (keyFile == null) {
@@ -747,15 +781,10 @@
// The blob is indeed an encrypted private key blob
String passwordSpec =
(keyPasswordSpec != null) ? keyPasswordSpec : PasswordRetriever.SPEC_STDIN;
- String keyPassword =
- passwordRetriver.getPassword(
+ List<char[]> keyPasswords =
+ passwordRetriver.getPasswords(
passwordSpec, "Private key password for " + name);
-
- PBEKeySpec decryptionKeySpec = new PBEKeySpec(keyPassword.toCharArray());
- SecretKey decryptionKey =
- SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName())
- .generateSecret(decryptionKeySpec);
- keySpec = encryptedPrivateKeyInfo.getKeySpec(decryptionKey);
+ keySpec = decryptPkcs8EncodedKey(encryptedPrivateKeyInfo, keyPasswords);
} catch (IOException e) {
// The blob is not an encrypted private key blob
if (keyPasswordSpec == null) {
@@ -788,6 +817,33 @@
this.certs = certList;
}
+ private static PKCS8EncodedKeySpec decryptPkcs8EncodedKey(
+ EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, List<char[]> passwords)
+ throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
+ SecretKeyFactory keyFactory =
+ SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName());
+ InvalidKeySpecException lastKeySpecException = null;
+ InvalidKeyException lastKeyException = null;
+ for (char[] password : passwords) {
+ PBEKeySpec decryptionKeySpec = new PBEKeySpec(password);
+ try {
+ SecretKey decryptionKey = keyFactory.generateSecret(decryptionKeySpec);
+ return encryptedPrivateKeyInfo.getKeySpec(decryptionKey);
+ } catch (InvalidKeySpecException e) {
+ lastKeySpecException = e;
+ } catch (InvalidKeyException e) {
+ lastKeyException = e;
+ }
+ }
+ if ((lastKeyException == null) && (lastKeySpecException == null)) {
+ throw new RuntimeException("No passwords");
+ } else if (lastKeyException != null) {
+ throw lastKeyException;
+ } else {
+ throw lastKeySpecException;
+ }
+ }
+
private static PrivateKey loadPkcs8EncodedPrivateKey(PKCS8EncodedKeySpec spec)
throws InvalidKeySpecException, NoSuchAlgorithmException {
try {
diff --git a/src/apksigner/java/com/android/apksigner/PasswordRetriever.java b/src/apksigner/java/com/android/apksigner/PasswordRetriever.java
index 25ef382..c09089d 100644
--- a/src/apksigner/java/com/android/apksigner/PasswordRetriever.java
+++ b/src/apksigner/java/com/android/apksigner/PasswordRetriever.java
@@ -16,14 +16,23 @@
package com.android.apksigner;
-import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
import java.io.Console;
import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
-import java.io.InputStreamReader;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
import java.nio.charset.Charset;
-import java.nio.file.Files;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
@@ -33,20 +42,23 @@
* input) which adds the need to keep some sources open across password retrievals. This class
* addresses the need.
*
- * <p>To use this retriever, construct a new instance, use the instance to retrieve passwords, and
- * then invoke {@link #clone()} on the instance when done, enabling the instance to close any
- * held resources.
+ * <p>To use this retriever, construct a new instance, use {@link #getPasswords(String, String)} to
+ * retrieve passwords, and then invoke {@link #close()} on the instance when done, enabling the
+ * instance to release any held resources.
*/
class PasswordRetriever implements AutoCloseable {
public static final String SPEC_STDIN = "stdin";
- private final Map<File, BufferedReader> mFileReaders = new HashMap<>();
- private BufferedReader mStdIn;
+ private static final Charset CONSOLE_CHARSET = getConsoleEncoding();
+
+ private final Map<File, InputStream> mFileInputStreams = new HashMap<>();
private boolean mClosed;
/**
- * Gets the password described by the provided spec.
+ * Returns the passwords described by the provided spec. The reason there may be more than one
+ * password is compatibility with {@code keytool} and {@code jarsigner} which in certain cases
+ * use the form of passwords encoded using the console's character encoding.
*
* <p>Supported specs:
* <ul>
@@ -61,46 +73,85 @@
* <p>When the same file (including standard input) is used for providing multiple passwords,
* the passwords are read from the file one line at a time.
*/
- public String getPassword(String spec, String description) throws IOException {
+ public List<char[]> getPasswords(String spec, String description) throws IOException {
+ // IMPLEMENTATION NOTE: Java KeyStore and PBEKeySpec APIs take passwords as arrays of
+ // Unicode characters (char[]). Unfortunately, it appears that Sun/Oracle keytool and
+ // jarsigner in some cases use passwords which are the encoded form obtained using the
+ // console's character encoding. For example, if the encoding is UTF-8, keytool and
+ // jarsigner will use the password which is obtained by upcasting each byte of the UTF-8
+ // encoded form to char. This occurs only when the password is read from stdin/console, and
+ // does not occur when the password is read from a command-line parameter.
+ // There are other tools which use the Java KeyStore API correctly.
+ // Thus, for each password spec, there may be up to three passwords:
+ // * Unicode characters,
+ // * characters (upcast bytes) obtained from encoding the password using the console's
+ // character encoding,
+ // * characters (upcast bytes) obtained from encoding the password using the JVM's default
+ // character encoding.
+ //
+ // For a sample password "\u0061\u0062\u00a1\u00e4\u044e\u0031":
+ // On Windows 10 with English US as the UI language, IBM437 is used as console encoding and
+ // windows-1252 is used as the JVM default encoding:
+ // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000
+ // -alias test
+ // generates a keystore and key which decrypt only with
+ // "\u0061\u0062\u00ad\u0084\u003f\u0031"
+ // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000
+ // -alias test -storepass <pass here>
+ // generates a keystore and key which decrypt only with
+ // "\u0061\u0062\u00a1\u00e4\u003f\u0031"
+ // On modern OSX/Linux UTF-8 is used as the console and JVM default encoding:
+ // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000
+ // -alias test
+ // generates a keystore and key which decrypt only with
+ // "\u0061\u0062\u00c2\u00a1\u00c3\u00a4\u00d1\u008e\u0031"
+ // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000
+ // -alias test
+ // generates a keystore and key which decrypt only with
+ // "\u0061\u0062\u00a1\u00e4\u044e\u0031"
+
assertNotClosed();
if (spec.startsWith("pass:")) {
- return spec.substring("pass:".length());
+ char[] pwd = spec.substring("pass:".length()).toCharArray();
+ return getPasswords(pwd);
} else if (SPEC_STDIN.equals(spec)) {
Console console = System.console();
if (console != null) {
- char[] password = console.readPassword(description + ": ");
- if (password == null) {
+ // Reading from console
+ char[] pwd = console.readPassword(description + ": ");
+ if (pwd == null) {
throw new IOException("Failed to read " + description + ": console closed");
}
- return new String(password);
+ return getPasswords(pwd);
+ } else {
+ // Console not available -- reading from redirected input
+ System.out.println(description + ": ");
+ byte[] encodedPwd = readEncodedPassword(System.in);
+ if (encodedPwd.length == 0) {
+ throw new IOException(
+ "Failed to read " + description + ": standard input closed");
+ }
+ // By default, textual input obtained via standard input is supposed to be decoded
+ // using the in JVM default character encoding but we also try the console's
+ // encoding just in case.
+ return getPasswords(encodedPwd, Charset.defaultCharset(), CONSOLE_CHARSET);
}
-
- if (mStdIn == null) {
- mStdIn =
- new BufferedReader(
- new InputStreamReader(System.in, Charset.defaultCharset()));
- }
- System.out.println(description + ":");
- String line = mStdIn.readLine();
- if (line == null) {
- throw new IOException(
- "Failed to read " + description + ": standard input closed");
- }
- return line;
} else if (spec.startsWith("file:")) {
String name = spec.substring("file:".length());
File file = new File(name).getCanonicalFile();
- BufferedReader in = mFileReaders.get(file);
+ InputStream in = mFileInputStreams.get(file);
if (in == null) {
- in = Files.newBufferedReader(file.toPath(), Charset.defaultCharset());
- mFileReaders.put(file, in);
+ in = new FileInputStream(file);
+ mFileInputStreams.put(file, in);
}
- String line = in.readLine();
- if (line == null) {
+ byte[] encodedPwd = readEncodedPassword(in);
+ if (encodedPwd.length == 0) {
throw new IOException(
"Failed to read " + description + " : end of file reached in " + file);
}
- return line;
+ // By default, textual input from files is supposed to be treated as encoded using JVM's
+ // default character encoding.
+ return getPasswords(encodedPwd, Charset.defaultCharset());
} else if (spec.startsWith("env:")) {
String name = spec.substring("env:".length());
String value = System.getenv(name);
@@ -109,12 +160,179 @@
"Failed to read " + description + ": environment variable " + value
+ " not specified");
}
- return value;
+ return getPasswords(value.toCharArray());
} else {
throw new IOException("Unsupported password spec for " + description + ": " + spec);
}
}
+ /**
+ * Returns the provided password and all password variants derived from the password. The
+ * resulting list is guaranteed to contain at least one element.
+ */
+ private static List<char[]> getPasswords(char[] pwd) {
+ List<char[]> passwords = new ArrayList<>(3);
+ addPasswords(passwords, pwd);
+ return passwords;
+ }
+
+ /**
+ * Returns the provided password and all password variants derived from the password. The
+ * resulting list is guaranteed to contain at least one element.
+ *
+ * @param encodedPwd password encoded using the provided character encoding.
+ * @param encodings character encodings in which the password is encoded in {@code encodedPwd}.
+ */
+ private static List<char[]> getPasswords(byte[] encodedPwd, Charset... encodings) {
+ List<char[]> passwords = new ArrayList<>(4);
+
+ for (Charset encoding : encodings) {
+ // Decode password and add it and its variants to the list
+ try {
+ char[] pwd = decodePassword(encodedPwd, encoding);
+ addPasswords(passwords, pwd);
+ } catch (IOException ignored) {}
+ }
+
+ // Add the original encoded form
+ addPassword(passwords, castBytesToChars(encodedPwd));
+ return passwords;
+ }
+
+ /**
+ * Adds the provided password and its variants to the provided list of passwords.
+ *
+ * <p>NOTE: This method adds only the passwords/variants which are not yet in the list.
+ */
+ private static void addPasswords(List<char[]> passwords, char[] pwd) {
+ // Verbatim password
+ addPassword(passwords, pwd);
+
+ // Password encoded using the JVM default character encoding and upcast into char[]
+ try {
+ char[] encodedPwd = castBytesToChars(encodePassword(pwd, Charset.defaultCharset()));
+ addPassword(passwords, encodedPwd);
+ } catch (IOException ignored) {}
+
+ // Password encoded using console character encoding and upcast into char[]
+ if (!CONSOLE_CHARSET.equals(Charset.defaultCharset())) {
+ try {
+ char[] encodedPwd = castBytesToChars(encodePassword(pwd, CONSOLE_CHARSET));
+ addPassword(passwords, encodedPwd);
+ } catch (IOException ignored) {}
+ }
+ }
+
+ /**
+ * Adds the provided password to the provided list. Does nothing if the password is already in
+ * the list.
+ */
+ private static void addPassword(List<char[]> passwords, char[] password) {
+ for (char[] existingPassword : passwords) {
+ if (Arrays.equals(password, existingPassword)) {
+ return;
+ }
+ }
+ passwords.add(password);
+ }
+
+ private static byte[] encodePassword(char[] pwd, Charset cs) throws IOException {
+ ByteBuffer pwdBytes =
+ cs.newEncoder()
+ .onMalformedInput(CodingErrorAction.REPLACE)
+ .onUnmappableCharacter(CodingErrorAction.REPLACE)
+ .encode(CharBuffer.wrap(pwd));
+ byte[] encoded = new byte[pwdBytes.remaining()];
+ pwdBytes.get(encoded);
+ return encoded;
+ }
+
+ private static char[] decodePassword(byte[] pwdBytes, Charset encoding) throws IOException {
+ CharBuffer pwdChars =
+ encoding.newDecoder()
+ .onMalformedInput(CodingErrorAction.REPLACE)
+ .onUnmappableCharacter(CodingErrorAction.REPLACE)
+ .decode(ByteBuffer.wrap(pwdBytes));
+ char[] result = new char[pwdChars.remaining()];
+ pwdChars.get(result);
+ return result;
+ }
+
+ /**
+ * Upcasts each {@code byte} in the provided array of bytes to a {@code char} and returns the
+ * resulting array of characters.
+ */
+ private static char[] castBytesToChars(byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+
+ char[] chars = new char[bytes.length];
+ for (int i = 0; i < bytes.length; i++) {
+ chars[i] = (char) (bytes[i] & 0xff);
+ }
+ return chars;
+ }
+
+ /**
+ * Returns the character encoding used by the console.
+ */
+ private static Charset getConsoleEncoding() {
+ // IMPLEMENTATION NOTE: There is no public API for obtaining the console's character
+ // encoding. We thus cheat by using implementation details of the most popular JVMs.
+ String consoleCharsetName;
+ try {
+ Method encodingMethod = Console.class.getDeclaredMethod("encoding");
+ encodingMethod.setAccessible(true);
+ consoleCharsetName = (String) encodingMethod.invoke(null);
+ if (consoleCharsetName == null) {
+ return Charset.defaultCharset();
+ }
+ } catch (ReflectiveOperationException e) {
+ Charset defaultCharset = Charset.defaultCharset();
+ System.err.println(
+ "warning: Failed to obtain console character encoding name. Assuming "
+ + defaultCharset);
+ return defaultCharset;
+ }
+
+ try {
+ return Charset.forName(consoleCharsetName);
+ } catch (IllegalArgumentException e) {
+ // On Windows 10, cp65001 is the UTF-8 code page. For some reason, popular JVMs don't
+ // have a mapping for cp65001...
+ if ("cp65001".equals(consoleCharsetName)) {
+ return StandardCharsets.UTF_8;
+ }
+ Charset defaultCharset = Charset.defaultCharset();
+ System.err.println(
+ "warning: Console uses unknown character encoding: " + consoleCharsetName
+ + ". Using " + defaultCharset + " instead");
+ return defaultCharset;
+ }
+ }
+
+ private static byte[] readEncodedPassword(InputStream in) throws IOException {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ int b;
+ while ((b = in.read()) != -1) {
+ if (b == '\n') {
+ break;
+ } else if (b == '\r') {
+ int next = in.read();
+ if ((next == -1) || (next == '\n')) {
+ break;
+ }
+
+ if (!(in instanceof PushbackInputStream)) {
+ in = new PushbackInputStream(in);
+ }
+ ((PushbackInputStream) in).unread(next);
+ }
+ result.write(b);
+ }
+ return result.toByteArray();
+ }
private void assertNotClosed() {
if (mClosed) {
@@ -124,20 +342,12 @@
@Override
public void close() {
- if (mStdIn != null) {
- try {
- mStdIn.close();
- } catch (IOException ignored) {
- } finally {
- mStdIn = null;
- }
- }
- for (BufferedReader in : mFileReaders.values()) {
+ for (InputStream in : mFileInputStreams.values()) {
try {
in.close();
} catch (IOException ignored) {}
}
- mFileReaders.clear();
+ mFileInputStreams.clear();
mClosed = true;
}
}