Home | History | Annotate | Download | only in apksigner
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.apksigner;
     18 
     19 import java.io.ByteArrayOutputStream;
     20 import java.io.Console;
     21 import java.io.File;
     22 import java.io.FileInputStream;
     23 import java.io.IOException;
     24 import java.io.InputStream;
     25 import java.io.PushbackInputStream;
     26 import java.lang.reflect.Method;
     27 import java.nio.ByteBuffer;
     28 import java.nio.CharBuffer;
     29 import java.nio.charset.Charset;
     30 import java.nio.charset.CodingErrorAction;
     31 import java.nio.charset.StandardCharsets;
     32 import java.util.ArrayList;
     33 import java.util.Arrays;
     34 import java.util.HashMap;
     35 import java.util.List;
     36 import java.util.Map;
     37 
     38 /**
     39  * Retriever of passwords based on password specs supported by {@code apksigner} tool.
     40  *
     41  * <p>apksigner supports retrieving multiple passwords from the same source (e.g., file, standard
     42  * input) which adds the need to keep some sources open across password retrievals. This class
     43  * addresses the need.
     44  *
     45  * <p>To use this retriever, construct a new instance, use
     46  * {@link #getPasswords(String, String, Charset...)} to retrieve passwords, and then invoke
     47  * {@link #close()} on the instance when done, enabling the instance to release any held resources.
     48  */
     49 class PasswordRetriever implements AutoCloseable {
     50     public static final String SPEC_STDIN = "stdin";
     51 
     52     /** Character encoding used by the console or {@code null} if not known. */
     53     private final Charset mConsoleEncoding;
     54 
     55     private final Map<File, InputStream> mFileInputStreams = new HashMap<>();
     56 
     57     private boolean mClosed;
     58 
     59     PasswordRetriever() {
     60         mConsoleEncoding = getConsoleEncoding();
     61     }
     62 
     63     /**
     64      * Returns the passwords described by the provided spec. The reason there may be more than one
     65      * password is compatibility with {@code keytool} and {@code jarsigner} which in certain cases
     66      * use the form of passwords encoded using the console's character encoding or the JVM default
     67      * encoding.
     68      *
     69      * <p>Supported specs:
     70      * <ul>
     71      * <li><em>stdin</em> -- read password as a line from console, if available, or standard
     72      *     input if console is not available</li>
     73      * <li><em>pass:password</em> -- password specified inside the spec, starting after
     74      *     {@code pass:}</li>
     75      * <li><em>file:path</em> -- read password as a line from the specified file</li>
     76      * <li><em>env:name</em> -- password is in the specified environment variable</li>
     77      * </ul>
     78      *
     79      * <p>When the same file (including standard input) is used for providing multiple passwords,
     80      * the passwords are read from the file one line at a time.
     81      *
     82      * @param additionalPwdEncodings additional encodings for converting the password into KeyStore
     83      *        or PKCS #8 encrypted key password. These encoding are used in addition to using the
     84      *        password verbatim or encoded using JVM default character encoding. A useful encoding
     85      *        to provide is the console character encoding on Windows machines where the console
     86      *        may be different from the JVM default encoding. Unfortunately, there is no public API
     87      *        to obtain the console's character encoding.
     88      */
     89     public List<char[]> getPasswords(
     90             String spec, String description, Charset... additionalPwdEncodings)
     91                     throws IOException {
     92         // IMPLEMENTATION NOTE: Java KeyStore and PBEKeySpec APIs take passwords as arrays of
     93         // Unicode characters (char[]). Unfortunately, it appears that Sun/Oracle keytool and
     94         // jarsigner in some cases use passwords which are the encoded form obtained using the
     95         // console's character encoding. For example, if the encoding is UTF-8, keytool and
     96         // jarsigner will use the password which is obtained by upcasting each byte of the UTF-8
     97         // encoded form to char. This occurs only when the password is read from stdin/console, and
     98         // does not occur when the password is read from a command-line parameter.
     99         // There are other tools which use the Java KeyStore API correctly.
    100         // Thus, for each password spec, a valid password is typically one of these three:
    101         // * Unicode characters,
    102         // * characters (upcast bytes) obtained from encoding the password using the console's
    103         //   character encoding of the console used on the environment where the KeyStore was
    104         //   created,
    105         // * characters (upcast bytes) obtained from encoding the password using the JVM's default
    106         //   character encoding of the machine where the KeyStore was created.
    107         //
    108         // For a sample password "\u0061\u0062\u00a1\u00e4\u044e\u0031":
    109         // On Windows 10 with English US as the UI language, IBM437 is used as console encoding and
    110         // windows-1252 is used as the JVM default encoding:
    111         // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000
    112         //     -alias test
    113         //   generates a keystore and key which decrypt only with
    114         //   "\u0061\u0062\u00ad\u0084\u003f\u0031"
    115         // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000
    116         //     -alias test -storepass <pass here>
    117         //   generates a keystore and key which decrypt only with
    118         //   "\u0061\u0062\u00a1\u00e4\u003f\u0031"
    119         // On modern OSX/Linux UTF-8 is used as the console and JVM default encoding:
    120         // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000
    121         //     -alias test
    122         //   generates a keystore and key which decrypt only with
    123         //   "\u0061\u0062\u00c2\u00a1\u00c3\u00a4\u00d1\u008e\u0031"
    124         // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000
    125         //     -alias test -storepass <pass here>
    126         //   generates a keystore and key which decrypt only with
    127         //   "\u0061\u0062\u00a1\u00e4\u044e\u0031"
    128         //
    129         // We optimize for the case where the KeyStore was created on the same machine where
    130         // apksigner is executed. Thus, we can assume the JVM default encoding used for creating the
    131         // KeyStore is the same as the current JVM's default encoding. We can make a similar
    132         // assumption about the console's encoding. However, there is no public API for obtaining
    133         // the console's character encoding. Prior to Java 9, we could cheat by using Reflection API
    134         // to access Console.encoding field. However, in the official Java 9 JVM this field is not
    135         // only inaccessible, but results in warnings being spewed to stdout during access attempts.
    136         // As a result, we cannot auto-detect the console's encoding and thus rely on the user to
    137         // explicitly provide it to apksigner as a command-line parameter (and passed into this
    138         // method as additionalPwdEncodings), if the password is using non-ASCII characters.
    139 
    140         assertNotClosed();
    141         if (spec.startsWith("pass:")) {
    142             char[] pwd = spec.substring("pass:".length()).toCharArray();
    143             return getPasswords(pwd, additionalPwdEncodings);
    144         } else if (SPEC_STDIN.equals(spec)) {
    145             Console console = System.console();
    146             if (console != null) {
    147                 // Reading from console
    148                 char[] pwd = console.readPassword(description + ": ");
    149                 if (pwd == null) {
    150                     throw new IOException("Failed to read " + description + ": console closed");
    151                 }
    152                 return getPasswords(pwd, additionalPwdEncodings);
    153             } else {
    154                 // Console not available -- reading from standard input
    155                 System.out.println(description + ": ");
    156                 byte[] encodedPwd = readEncodedPassword(System.in);
    157                 if (encodedPwd.length == 0) {
    158                     throw new IOException(
    159                             "Failed to read " + description + ": standard input closed");
    160                 }
    161                 // By default, textual input obtained via standard input is supposed to be decoded
    162                 // using the in JVM default character encoding.
    163                 return getPasswords(encodedPwd, Charset.defaultCharset(), additionalPwdEncodings);
    164             }
    165         } else if (spec.startsWith("file:")) {
    166             String name = spec.substring("file:".length());
    167             File file = new File(name).getCanonicalFile();
    168             InputStream in = mFileInputStreams.get(file);
    169             if (in == null) {
    170                 in = new FileInputStream(file);
    171                 mFileInputStreams.put(file, in);
    172             }
    173             byte[] encodedPwd = readEncodedPassword(in);
    174             if (encodedPwd.length == 0) {
    175                 throw new IOException(
    176                         "Failed to read " + description + " : end of file reached in " + file);
    177             }
    178             // By default, textual input from files is supposed to be treated as encoded using JVM's
    179             // default character encoding.
    180             return getPasswords(encodedPwd, Charset.defaultCharset(), additionalPwdEncodings);
    181         } else if (spec.startsWith("env:")) {
    182             String name = spec.substring("env:".length());
    183             String value = System.getenv(name);
    184             if (value == null) {
    185                 throw new IOException(
    186                         "Failed to read " + description + ": environment variable " + value
    187                                 + " not specified");
    188             }
    189             return getPasswords(value.toCharArray(), additionalPwdEncodings);
    190         } else {
    191             throw new IOException("Unsupported password spec for " + description + ": " + spec);
    192         }
    193     }
    194 
    195     /**
    196      * Returns the provided password and all password variants derived from the password. The
    197      * resulting list is guaranteed to contain at least one element.
    198      */
    199     private List<char[]> getPasswords(char[] pwd, Charset... additionalEncodings) {
    200         List<char[]> passwords = new ArrayList<>(3);
    201         addPasswords(passwords, pwd, additionalEncodings);
    202         return passwords;
    203     }
    204 
    205     /**
    206      * Returns the provided password and all password variants derived from the password. The
    207      * resulting list is guaranteed to contain at least one element.
    208      *
    209      * @param encodedPwd password encoded using {@code encodingForDecoding}.
    210      */
    211     private List<char[]> getPasswords(
    212             byte[] encodedPwd, Charset encodingForDecoding,
    213             Charset... additionalEncodings) {
    214         List<char[]> passwords = new ArrayList<>(4);
    215 
    216         // Decode password and add it and its variants to the list
    217         try {
    218             char[] pwd = decodePassword(encodedPwd, encodingForDecoding);
    219             addPasswords(passwords, pwd, additionalEncodings);
    220         } catch (IOException ignored) {}
    221 
    222         // Add the original encoded form
    223         addPassword(passwords, castBytesToChars(encodedPwd));
    224         return passwords;
    225     }
    226 
    227     /**
    228      * Adds the provided password and its variants to the provided list of passwords.
    229      *
    230      * <p>NOTE: This method adds only the passwords/variants which are not yet in the list.
    231      */
    232     private void addPasswords(List<char[]> passwords, char[] pwd, Charset... additionalEncodings) {
    233         if ((additionalEncodings != null) && (additionalEncodings.length > 0)) {
    234             for (Charset encoding : additionalEncodings) {
    235                 // Password encoded using provided encoding (usually the console's character
    236                 // encoding) and upcast into char[]
    237                 try {
    238                     char[] encodedPwd = castBytesToChars(encodePassword(pwd, encoding));
    239                     addPassword(passwords, encodedPwd);
    240                 } catch (IOException ignored) {}
    241             }
    242         }
    243 
    244         // Verbatim password
    245         addPassword(passwords, pwd);
    246 
    247         // Password encoded using the console encoding and upcast into char[]
    248         if (mConsoleEncoding != null) {
    249             try {
    250                 char[] encodedPwd = castBytesToChars(encodePassword(pwd, mConsoleEncoding));
    251                 addPassword(passwords, encodedPwd);
    252             } catch (IOException ignored) {}
    253         }
    254 
    255         // Password encoded using the JVM default character encoding and upcast into char[]
    256         try {
    257             char[] encodedPwd = castBytesToChars(encodePassword(pwd, Charset.defaultCharset()));
    258             addPassword(passwords, encodedPwd);
    259         } catch (IOException ignored) {}
    260     }
    261 
    262     /**
    263      * Adds the provided password to the provided list. Does nothing if the password is already in
    264      * the list.
    265      */
    266     private static void addPassword(List<char[]> passwords, char[] password) {
    267         for (char[] existingPassword : passwords) {
    268             if (Arrays.equals(password, existingPassword)) {
    269                 return;
    270             }
    271         }
    272         passwords.add(password);
    273     }
    274 
    275     private static byte[] encodePassword(char[] pwd, Charset cs) throws IOException {
    276         ByteBuffer pwdBytes =
    277                 cs.newEncoder()
    278                 .onMalformedInput(CodingErrorAction.REPLACE)
    279                 .onUnmappableCharacter(CodingErrorAction.REPLACE)
    280                 .encode(CharBuffer.wrap(pwd));
    281         byte[] encoded = new byte[pwdBytes.remaining()];
    282         pwdBytes.get(encoded);
    283         return encoded;
    284     }
    285 
    286     private static char[] decodePassword(byte[] pwdBytes, Charset encoding) throws IOException {
    287         CharBuffer pwdChars =
    288                 encoding.newDecoder()
    289                 .onMalformedInput(CodingErrorAction.REPLACE)
    290                 .onUnmappableCharacter(CodingErrorAction.REPLACE)
    291                 .decode(ByteBuffer.wrap(pwdBytes));
    292         char[] result = new char[pwdChars.remaining()];
    293         pwdChars.get(result);
    294         return result;
    295     }
    296 
    297     /**
    298      * Upcasts each {@code byte} in the provided array of bytes to a {@code char} and returns the
    299      * resulting array of characters.
    300      */
    301     private static char[] castBytesToChars(byte[] bytes) {
    302         if (bytes == null) {
    303             return null;
    304         }
    305 
    306         char[] chars = new char[bytes.length];
    307         for (int i = 0; i < bytes.length; i++) {
    308             chars[i] = (char) (bytes[i] & 0xff);
    309         }
    310         return chars;
    311     }
    312 
    313     private static boolean isJava9OrHigherErrOnTheSideOfCaution() {
    314         // Before Java 9, this string is of major.minor form, such as "1.8" for Java 8.
    315         // From Java 9 onwards, this is a single number: major, such as "9" for Java 9.
    316         // See JEP 223: New Version-String Scheme.
    317 
    318         String versionString = System.getProperty("java.specification.version");
    319         if (versionString == null) {
    320             // Better safe than sorry
    321             return true;
    322         }
    323         return !versionString.startsWith("1.");
    324     }
    325 
    326     /**
    327      * Returns the character encoding used by the console or {@code null} if the encoding is not
    328      * known.
    329      */
    330     private static Charset getConsoleEncoding() {
    331         // IMPLEMENTATION NOTE: There is no public API for obtaining the console's character
    332         // encoding. We thus cheat by using implementation details of the most popular JVMs.
    333         // Unfortunately, this doesn't work on Java 9 JVMs where access to Console.encoding is
    334         // restricted by default and leads to spewing to stdout at runtime.
    335         if (isJava9OrHigherErrOnTheSideOfCaution()) {
    336             return null;
    337         }
    338         String consoleCharsetName = null;
    339         try {
    340             Method encodingMethod = Console.class.getDeclaredMethod("encoding");
    341             encodingMethod.setAccessible(true);
    342             consoleCharsetName = (String) encodingMethod.invoke(null);
    343         } catch (ReflectiveOperationException ignored) {
    344             return null;
    345         }
    346 
    347         if (consoleCharsetName == null) {
    348             // Console encoding is the same as this JVM's default encoding
    349             return Charset.defaultCharset();
    350         }
    351 
    352         try {
    353             return getCharsetByName(consoleCharsetName);
    354         } catch (IllegalArgumentException e) {
    355             return null;
    356         }
    357     }
    358 
    359     public static Charset getCharsetByName(String charsetName) throws IllegalArgumentException {
    360         // On Windows 10, cp65001 is the UTF-8 code page. For some reason, popular JVMs don't
    361         // have a mapping for cp65001...
    362         if ("cp65001".equalsIgnoreCase(charsetName)) {
    363             return StandardCharsets.UTF_8;
    364         }
    365         return Charset.forName(charsetName);
    366     }
    367 
    368     private static byte[] readEncodedPassword(InputStream in) throws IOException {
    369         ByteArrayOutputStream result = new ByteArrayOutputStream();
    370         int b;
    371         while ((b = in.read()) != -1) {
    372             if (b == '\n') {
    373                 break;
    374             } else if (b == '\r') {
    375                 int next = in.read();
    376                 if ((next == -1) || (next == '\n')) {
    377                     break;
    378                 }
    379 
    380                 if (!(in instanceof PushbackInputStream)) {
    381                     in = new PushbackInputStream(in);
    382                 }
    383                 ((PushbackInputStream) in).unread(next);
    384             }
    385             result.write(b);
    386         }
    387         return result.toByteArray();
    388     }
    389 
    390     private void assertNotClosed() {
    391         if (mClosed) {
    392             throw new IllegalStateException("Closed");
    393         }
    394     }
    395 
    396     @Override
    397     public void close() {
    398         for (InputStream in : mFileInputStreams.values()) {
    399             try {
    400                 in.close();
    401             } catch (IOException ignored) {}
    402         }
    403         mFileInputStreams.clear();
    404         mClosed = true;
    405     }
    406 }
    407