[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#857407: unblock android-platform-tools-apksig/0.5+git165~g42d07eb-1



Package: release.debian.org
Severity: normal
User: release.debian.org@packages.debian.org
Usertags: unblock

Please unblock package: android-platform-tools-apksig

This is the next upstream release, which only fixes the password issues
as described in #857027.  Upstream still doesn't have release tags,
hence the version string.  I took this opportunity to include the
bash-completion to the package as well, for completeness.

Attached is the debdiff.
diff --git a/debian/apksigner.bash-completion b/debian/apksigner.bash-completion
new file mode 100644
index 0000000..af6f4e3
--- /dev/null
+++ b/debian/apksigner.bash-completion
@@ -0,0 +1 @@
+debian/bash-completion/apksigner
diff --git a/debian/bash-completion/apksigner b/debian/bash-completion/apksigner
new file mode 100644
index 0000000..d68ddbe
--- /dev/null
+++ b/debian/bash-completion/apksigner
@@ -0,0 +1,95 @@
+# Debian apksigner completion                             -*- shell-script -*-
+
+_apksigner()
+{
+    local cur prev words cword
+    _init_completion || return
+
+    local GENERIC_OPTIONS='
+        --cert
+        -h --help
+        --in
+        --key
+        --key-pass
+        --ks
+        --ks-key-alias
+        --ks-pass
+        --ks-provider-arg
+        --ks-provider-class
+        --ks-provider-name
+        --ks-type
+        --max-sdk-version
+        --min-sdk-version
+        --next-signer
+        --out
+        --print-certs
+        --v1-signer-name
+        --v1-signing-enabled
+        --v2-signing-enabled
+        -v --verbose
+        --Werr
+    '
+
+    # see if the user selected a command already
+    local COMMANDS=(
+        "help"
+        "sign"
+        "verify"
+        "version")
+
+    local command i
+    for (( i=0; i < ${#words[@]}-1; i++ )); do
+        if [[ ${COMMANDS[@]} =~ ${words[i]} ]]; then
+            command=${words[i]}
+            break
+        fi
+    done
+
+    # Complete a --option<SPACE><TAB>
+    case $prev in
+        --in|--out)
+            _filedir '@(apk|jar)'
+            return 0
+            ;;
+        --ks)
+            _filedir '@(bks|jks|keystore)'
+            return 0
+            ;;
+    esac
+
+    # supported options per command
+    if [[ "$cur" == -* ]]; then
+        case $command in
+            sign|verify)
+                COMPREPLY=( $( compgen -W "$GENERIC_OPTIONS" -- "$cur" ) )
+                return 0
+                ;;
+            help)
+                return 0
+                ;;
+            version)
+                return 0
+                ;;
+        esac
+    fi
+
+    # specific command arguments
+    if [[ -n $command ]]; then
+        case $command in
+            sign|verify)
+                _filedir '@(apk|jar)'
+                return 0
+                ;;
+        esac
+    fi
+
+    # no command yet, show what commands we have
+    if [ "$command" = "" ]; then
+        COMPREPLY=( $( compgen -W '${COMMANDS[@]}' -- "$cur" ) )
+    fi
+
+    return 0
+} &&
+complete -F _apksigner apksigner
+
+# ex: ts=4 sw=4 et filetype=sh
diff --git a/debian/changelog b/debian/changelog
index 7b27e37..eb91c32 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+android-platform-tools-apksig (0.5+git165~g42d07eb-1) unstable; urgency=medium
+
+  * New upstream release (Closes: #857027)
+  * Add bash-completion
+
+ -- Hans-Christoph Steiner <hans@eds.org>  Fri, 10 Mar 2017 13:58:11 +0100
+
 android-platform-tools-apksig (0.4+git162~g85a854b-1) unstable; urgency=medium
 
   * New upstream release
diff --git a/debian/control b/debian/control
index a099c2d..3c9f8a8 100644
--- a/debian/control
+++ b/debian/control
@@ -4,6 +4,7 @@ Priority: optional
 Maintainer: Android Tools Maintainers <android-tools-devel@lists.alioth.debian.org>
 Uploaders: Hans-Christoph Steiner <hans@eds.org>
 Build-Depends: antlr3,
+               bash-completion,
                debhelper (>= 10),
                default-jdk-headless | default-jdk (>= 1:1.6),
                gradle-debian-helper,
diff --git a/debian/rules b/debian/rules
index 4902a7d..f4911e3 100755
--- a/debian/rules
+++ b/debian/rules
@@ -7,9 +7,9 @@ export JAVA_HOME=/usr/lib/jvm/default-java
 export CLASSPATH=/usr/share/java/apksig.jar
 
 %:
-	dh $@ --with maven_repo_helper,javahelper --buildsystem=gradle
+	dh $@ --with maven_repo_helper,javahelper,bash-completion --buildsystem=gradle
 
-tarball_name = 85a854b038c28fa2b34eaee0ff34e67c164880ea
+tarball_name = 42d07eb
 
 override_dh_auto_build: debian/apksigner.1
 	dh_auto_build
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index 745fe39..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.io.PrintStream;
 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 @@ import javax.crypto.spec.PBEKeySpec;
  */
 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,31 +623,20 @@ public class ApkSignerTool {
             }
 
             // 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
-            char[] keyPwd;
-            if (keyPasswordSpec == null) {
-                keyPwd = keystorePwd;
-            } else {
-                keyPwd =
-                        passwordRetriever.getPassword(keyPasswordSpec, "Key password for " + name)
-                                .toCharArray();
-            }
             String keyAlias = null;
             PrivateKey key = null;
             try {
@@ -680,25 +671,32 @@ public class ApkSignerTool {
                     throw new ParameterException(
                             keystoreFile + " entry \"" + keyAlias + "\" does not contain a key");
                 }
+
                 Key entryKey;
-                if (keyPwd != null) {
-                    // Key password specified -- load this key as a password-protected key
-                    entryKey = ks.getKey(keyAlias, keyPwd);
+                if (keyPasswordSpec != null) {
+                    // Key password spec is explicitly specified. Use this spec to obtain the
+                    // password and then load the key using that password.
+                    List<char[]> keyPasswords =
+                            passwordRetriever.getPasswords(
+                                    keyPasswordSpec,
+                                    "Key \"" + keyAlias + "\" password for " + name);
+                    entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords);
                 } else {
-                    // Key password not specified -- try to load this key without using a password
+                    // 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, null);
+                        entryKey = getKeyStoreKey(ks, keyAlias, keystorePasswords);
                     } catch (UnrecoverableKeyException expected) {
-                        // Looks like this might be a password-protected key. Prompt for password
-                        // and try loading the key using the password.
-                        keyPwd =
-                                passwordRetriever.getPassword(
+                        List<char[]> keyPasswords =
+                                passwordRetriever.getPasswords(
                                         PasswordRetriever.SPEC_STDIN,
-                                        "Password for key with alias \"" + keyAlias + "\"")
-                                                .toCharArray();
-                        entryKey = ks.getKey(keyAlias, keyPwd);
+                                        "Key \"" + keyAlias + "\" password for " + name);
+                        entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords);
                     }
                 }
+
                 if (entryKey == null) {
                     throw new ParameterException(
                             keystoreFile + " entry \"" + keyAlias + "\" does not contain a key");
@@ -727,6 +725,43 @@ public class ApkSignerTool {
             }
         }
 
+        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) {
@@ -746,15 +781,10 @@ public class ApkSignerTool {
                 // 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) {
@@ -787,6 +817,33 @@ public class ApkSignerTool {
             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 @@ import java.util.Map;
  * 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 @@ class PasswordRetriever implements AutoCloseable {
      * <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);
-            }
-
-            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 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);
             }
-            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 @@ class PasswordRetriever implements AutoCloseable {
                         "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 @@ class PasswordRetriever implements AutoCloseable {
 
     @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;
     }
 }
diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt
index a673ea9..ad865fe 100644
--- a/src/apksigner/java/com/android/apksigner/help_sign.txt
+++ b/src/apksigner/java/com/android/apksigner/help_sign.txt
@@ -85,9 +85,7 @@ file in X.509 format (see --key and --cert).
                       signer, KeyStore password is read before the key password
                       is read.
 
---key-pass            Password with which the private key is protected. By
-                      default it is assumed that KeyStore keys are protected
-                      using the same password as their KeyStore (see --ks-pass).
+--key-pass            Password with which the private key is protected.
                       The following formats are supported:
                           pass:<password> password provided inline
                           env:<name>      password provided in the named
@@ -96,8 +94,13 @@ file in X.509 format (see --key and --cert).
                                           file, as a single line
                           stdin           password provided on standard input,
                                           as a single line
-                      By default, if the key is password-protected, the tool
-                      will prompt for password via console or standard input.
+                      If --key-pass is not specified for a KeyStore key, this
+                      tool will attempt to load the key using the KeyStore
+                      password and, if that fails, will prompt for key password
+                      and attempt to load the key using that password.
+                      If --key-pass is not specified for a private key file key,
+                      this tool will prompt for key password only if a password
+                      is required.
                       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. Passwords are read in the

Reply to: