Add --pass-encoding parameter to apksigner am: 76d14b580f
am: 0e441b18df

Change-Id: I809bd7db2ede43fa464a575ab1cd2d3b4debfd6b
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index 9aed804..408ad5b 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -28,6 +28,7 @@
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintStream;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
@@ -161,6 +162,16 @@
                         optionsParser.getRequiredValue("KeyStore password");
             } else if ("key-pass".equals(optionName)) {
                 signerParams.keyPasswordSpec = optionsParser.getRequiredValue("Key password");
+            } else if ("pass-encoding".equals(optionName)) {
+                String charsetName =
+                        optionsParser.getRequiredValue("Password character encoding");
+                try {
+                    signerParams.passwordCharset = PasswordRetriever.getCharsetByName(charsetName);
+                } catch (IllegalArgumentException e) {
+                    throw new ParameterException(
+                            "Unsupported password character encoding requested using"
+                                    + " --pass-encoding: " + charsetName);
+                }
             } else if ("v1-signer-name".equals(optionName)) {
                 signerParams.v1SigFileBasename =
                         optionsParser.getRequiredValue("JAR signature file basename");
@@ -604,6 +615,7 @@
         String keystoreKeyAlias;
         String keystorePasswordSpec;
         String keyPasswordSpec;
+        Charset passwordCharset;
         String keystoreType;
         String keystoreProviderName;
         String keystoreProviderClass;
@@ -623,6 +635,7 @@
                     && (keystoreKeyAlias == null)
                     && (keystorePasswordSpec == null)
                     && (keyPasswordSpec == null)
+                    && (passwordCharset == null)
                     && (keystoreType == null)
                     && (keystoreProviderName == null)
                     && (keystoreProviderClass == null)
@@ -690,13 +703,19 @@
 
             // 2. Load the KeyStore
             List<char[]> keystorePasswords;
+            Charset[] additionalPasswordEncodings;
             {
                 String keystorePasswordSpec =
                         (this.keystorePasswordSpec != null)
                                 ?  this.keystorePasswordSpec : PasswordRetriever.SPEC_STDIN;
+                additionalPasswordEncodings =
+                        (passwordCharset != null)
+                                ? new Charset[] {passwordCharset} : new Charset[0];
                 keystorePasswords =
                         passwordRetriever.getPasswords(
-                                keystorePasswordSpec, "Keystore password for " + name);
+                                keystorePasswordSpec,
+                                "Keystore password for " + name,
+                                additionalPasswordEncodings);
                 loadKeyStoreFromFile(
                         ks,
                         "NONE".equals(keystoreFile) ? null : keystoreFile,
@@ -746,7 +765,8 @@
                     List<char[]> keyPasswords =
                             passwordRetriever.getPasswords(
                                     keyPasswordSpec,
-                                    "Key \"" + keyAlias + "\" password for " + name);
+                                    "Key \"" + keyAlias + "\" password for " + name,
+                                    additionalPasswordEncodings);
                     entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords);
                 } else {
                     // Key password spec is not specified. This means we should assume that key
@@ -759,7 +779,8 @@
                         List<char[]> keyPasswords =
                                 passwordRetriever.getPasswords(
                                         PasswordRetriever.SPEC_STDIN,
-                                        "Key \"" + keyAlias + "\" password for " + name);
+                                        "Key \"" + keyAlias + "\" password for " + name,
+                                        additionalPasswordEncodings);
                         entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords);
                     }
                 }
@@ -858,9 +879,14 @@
                 // The blob is indeed an encrypted private key blob
                 String passwordSpec =
                         (keyPasswordSpec != null) ? keyPasswordSpec : PasswordRetriever.SPEC_STDIN;
+                Charset[] additionalPasswordEncodings =
+                        (passwordCharset != null)
+                                ? new Charset[] {passwordCharset} : new Charset[0];
                 List<char[]> keyPasswords =
                         passwordRetriver.getPasswords(
-                                passwordSpec, "Private key password for " + name);
+                                passwordSpec,
+                                "Private key password for " + name,
+                                additionalPasswordEncodings);
                 keySpec = decryptPkcs8EncodedKey(encryptedPrivateKeyInfo, keyPasswords);
             } catch (IOException e) {
                 // The blob is not an encrypted private key blob
diff --git a/src/apksigner/java/com/android/apksigner/PasswordRetriever.java b/src/apksigner/java/com/android/apksigner/PasswordRetriever.java
index c09089d..83437ed 100644
--- a/src/apksigner/java/com/android/apksigner/PasswordRetriever.java
+++ b/src/apksigner/java/com/android/apksigner/PasswordRetriever.java
@@ -42,23 +42,29 @@
  * 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 {@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.
+ * <p>To use this retriever, construct a new instance, use
+ * {@link #getPasswords(String, String, Charset...)} 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 static final Charset CONSOLE_CHARSET = getConsoleEncoding();
+    /** Character encoding used by the console or {@code null} if not known. */
+    private final Charset mConsoleEncoding;
 
     private final Map<File, InputStream> mFileInputStreams = new HashMap<>();
 
     private boolean mClosed;
 
+    PasswordRetriever() {
+        mConsoleEncoding = getConsoleEncoding();
+    }
+
     /**
      * 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.
+     * use the form of passwords encoded using the console's character encoding or the JVM default
+     * encoding.
      *
      * <p>Supported specs:
      * <ul>
@@ -72,8 +78,17 @@
      *
      * <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.
+     *
+     * @param additionalPwdEncodings additional encodings for converting the password into KeyStore
+     *        or PKCS #8 encrypted key password. These encoding are used in addition to using the
+     *        password verbatim or encoded using JVM default character encoding. A useful encoding
+     *        to provide is the console character encoding on Windows machines where the console
+     *        may be different from the JVM default encoding. Unfortunately, there is no public API
+     *        to obtain the console's character encoding.
      */
-    public List<char[]> getPasswords(String spec, String description) throws IOException {
+    public List<char[]> getPasswords(
+            String spec, String description, Charset... additionalPwdEncodings)
+                    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
@@ -82,12 +97,13 @@
         // 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:
+        // Thus, for each password spec, a valid password is typically one of these three:
         // * Unicode characters,
         // * characters (upcast bytes) obtained from encoding the password using the console's
-        //   character encoding,
+        //   character encoding of the console used on the environment where the KeyStore was
+        //   created,
         // * characters (upcast bytes) obtained from encoding the password using the JVM's default
-        //   character encoding.
+        //   character encoding of the machine where the KeyStore was created.
         //
         // 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
@@ -106,14 +122,25 @@
         //   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
+        //     -alias test -storepass <pass here>
         //   generates a keystore and key which decrypt only with
         //   "\u0061\u0062\u00a1\u00e4\u044e\u0031"
+        //
+        // We optimize for the case where the KeyStore was created on the same machine where
+        // apksigner is executed. Thus, we can assume the JVM default encoding used for creating the
+        // KeyStore is the same as the current JVM's default encoding. We can make a similar
+        // assumption about the console's encoding. However, there is no public API for obtaining
+        // the console's character encoding. Prior to Java 9, we could cheat by using Reflection API
+        // to access Console.encoding field. However, in the official Java 9 JVM this field is not
+        // only inaccessible, but results in warnings being spewed to stdout during access attempts.
+        // As a result, we cannot auto-detect the console's encoding and thus rely on the user to
+        // explicitly provide it to apksigner as a command-line parameter (and passed into this
+        // method as additionalPwdEncodings), if the password is using non-ASCII characters.
 
         assertNotClosed();
         if (spec.startsWith("pass:")) {
             char[] pwd = spec.substring("pass:".length()).toCharArray();
-            return getPasswords(pwd);
+            return getPasswords(pwd, additionalPwdEncodings);
         } else if (SPEC_STDIN.equals(spec)) {
             Console console = System.console();
             if (console != null) {
@@ -122,9 +149,9 @@
                 if (pwd == null) {
                     throw new IOException("Failed to read " + description + ": console closed");
                 }
-                return getPasswords(pwd);
+                return getPasswords(pwd, additionalPwdEncodings);
             } else {
-                // Console not available -- reading from redirected input
+                // Console not available -- reading from standard input
                 System.out.println(description + ": ");
                 byte[] encodedPwd = readEncodedPassword(System.in);
                 if (encodedPwd.length == 0) {
@@ -132,9 +159,8 @@
                             "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);
+                // using the in JVM default character encoding.
+                return getPasswords(encodedPwd, Charset.defaultCharset(), additionalPwdEncodings);
             }
         } else if (spec.startsWith("file:")) {
             String name = spec.substring("file:".length());
@@ -151,7 +177,7 @@
             }
             // By default, textual input from files is supposed to be treated as encoded using JVM's
             // default character encoding.
-            return getPasswords(encodedPwd, Charset.defaultCharset());
+            return getPasswords(encodedPwd, Charset.defaultCharset(), additionalPwdEncodings);
         } else if (spec.startsWith("env:")) {
             String name = spec.substring("env:".length());
             String value = System.getenv(name);
@@ -160,7 +186,7 @@
                         "Failed to read " + description + ": environment variable " + value
                                 + " not specified");
             }
-            return getPasswords(value.toCharArray());
+            return getPasswords(value.toCharArray(), additionalPwdEncodings);
         } else {
             throw new IOException("Unsupported password spec for " + description + ": " + spec);
         }
@@ -170,9 +196,9 @@
      * 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) {
+    private List<char[]> getPasswords(char[] pwd, Charset... additionalEncodings) {
         List<char[]> passwords = new ArrayList<>(3);
-        addPasswords(passwords, pwd);
+        addPasswords(passwords, pwd, additionalEncodings);
         return passwords;
     }
 
@@ -180,19 +206,18 @@
      * 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}.
+     * @param encodedPwd password encoded using {@code encodingForDecoding}.
      */
-    private static List<char[]> getPasswords(byte[] encodedPwd, Charset... encodings) {
+    private List<char[]> getPasswords(
+            byte[] encodedPwd, Charset encodingForDecoding,
+            Charset... additionalEncodings) {
         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) {}
-        }
+        // Decode password and add it and its variants to the list
+        try {
+            char[] pwd = decodePassword(encodedPwd, encodingForDecoding);
+            addPasswords(passwords, pwd, additionalEncodings);
+        } catch (IOException ignored) {}
 
         // Add the original encoded form
         addPassword(passwords, castBytesToChars(encodedPwd));
@@ -204,23 +229,34 @@
      *
      * <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) {
+    private void addPasswords(List<char[]> passwords, char[] pwd, Charset... additionalEncodings) {
+        if ((additionalEncodings != null) && (additionalEncodings.length > 0)) {
+            for (Charset encoding : additionalEncodings) {
+                // Password encoded using provided encoding (usually the console's character
+                // encoding) and upcast into char[]
+                try {
+                    char[] encodedPwd = castBytesToChars(encodePassword(pwd, encoding));
+                    addPassword(passwords, encodedPwd);
+                } catch (IOException ignored) {}
+            }
+        }
+
         // Verbatim password
         addPassword(passwords, pwd);
 
+        // Password encoded using the console encoding and upcast into char[]
+        if (mConsoleEncoding != null) {
+            try {
+                char[] encodedPwd = castBytesToChars(encodePassword(pwd, mConsoleEncoding));
+                addPassword(passwords, encodedPwd);
+            } catch (IOException ignored) {}
+        }
+
         // 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) {}
-        }
     }
 
     /**
@@ -274,44 +310,61 @@
         return chars;
     }
 
+    private static boolean isJava9OrHigherErrOnTheSideOfCaution() {
+        // Before Java 9, this string is of major.minor form, such as "1.8" for Java 8.
+        // From Java 9 onwards, this is a single number: major, such as "9" for Java 9.
+        // See JEP 223: New Version-String Scheme.
+
+        String versionString = System.getProperty("java.specification.version");
+        if (versionString == null) {
+            // Better safe than sorry
+            return true;
+        }
+        return !versionString.startsWith("1.");
+    }
+
     /**
-     * Returns the character encoding used by the console.
+     * Returns the character encoding used by the console or {@code null} if the encoding is not
+     * known.
      */
     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;
+        // Unfortunately, this doesn't work on Java 9 JVMs where access to Console.encoding is
+        // restricted by default and leads to spewing to stdout at runtime.
+        if (isJava9OrHigherErrOnTheSideOfCaution()) {
+            return null;
+        }
+        String consoleCharsetName = null;
         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;
+        } catch (ReflectiveOperationException ignored) {
+            return null;
+        }
+
+        if (consoleCharsetName == null) {
+            // Console encoding is the same as this JVM's default encoding
+            return Charset.defaultCharset();
         }
 
         try {
-            return Charset.forName(consoleCharsetName);
+            return getCharsetByName(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;
+            return null;
         }
     }
 
+    public static Charset getCharsetByName(String charsetName) throws IllegalArgumentException {
+        // On Windows 10, cp65001 is the UTF-8 code page. For some reason, popular JVMs don't
+        // have a mapping for cp65001...
+        if ("cp65001".equalsIgnoreCase(charsetName)) {
+            return StandardCharsets.UTF_8;
+        }
+        return Charset.forName(charsetName);
+    }
+
     private static byte[] readEncodedPassword(InputStream in) throws IOException {
         ByteArrayOutputStream result = new ByteArrayOutputStream();
         int b;
diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt
index 91fcc49..80c5fa4 100644
--- a/src/apksigner/java/com/android/apksigner/help_sign.txt
+++ b/src/apksigner/java/com/android/apksigner/help_sign.txt
@@ -108,6 +108,21 @@
                       signer, KeyStore password is read before the key password
                       is read.
 
+--pass-encoding       Additional character encoding (e.g., ibm437 or utf-8) to
+                      try for passwords containing non-ASCII characters.
+                      KeyStores created by keytool are often encrypted not using
+                      the Unicode form of the password but rather using the form
+                      produced by encoding the password using the console's
+                      character encoding. apksigner by default tries to decrypt
+                      using several forms of the password: the Unicode form, the
+                      form encoded using the JVM default charset, and, on Java 8
+                      and older, the form encoded using the console's charset.
+                      On Java 9, apksigner cannot detect the console's charset
+                      and may need to be provided with --pass-encoding when a
+                      non-ASCII password is used. --pass-encoding may also need
+                      to be provided for a KeyStore created by keytool on a
+                      different OS or in a different locale.
+
 --ks-type             Type/algorithm of KeyStore to use. By default, the default
                       type is used.
 
@@ -171,3 +186,12 @@
 5. Sign an APK using PKCS #11 JCA Provider:
 $ apksigner sign --provider-class sun.security.pkcs11.SunPKCS11 \
     --provider-arg token.cfg --ks NONE --ks-type PKCS11 app.apk
+
+6. Sign an APK using a non-ASCII password KeyStore created on English Windows.
+   The --pass-encoding parameter is not needed if apksigner is being run on
+   English Windows with Java 8 or older.
+$ apksigner sign --ks release.jks --pass-encoding ibm437 app.apk
+
+7. Sign an APK on Windows using a non-ASCII password KeyStore created on a
+   modern OSX or Linux machine:
+$ apksigner sign --ks release.jks --pass-encoding utf-8 app.apk