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: