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

Bug#857407: marked as done (unblock android-platform-tools-apksig/0.5+git165~g42d07eb-1)



Your message dated Sat, 11 Mar 2017 11:06:01 +0000
with message-id <E1cmeqX-00039P-5m@respighi.debian.org>
and subject line unblock android-platform-tools-apksig
has caused the Debian Bug report #857407,
regarding unblock android-platform-tools-apksig/0.5+git165~g42d07eb-1
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact owner@bugs.debian.org
immediately.)


-- 
857407: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=857407
Debian Bug Tracking System
Contact owner@bugs.debian.org with problems
--- Begin Message ---
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

--- End Message ---
--- Begin Message ---
Unblocked.

--- End Message ---

Reply to: