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 {@link #getPasswords(String, String, 46 * Charset...)} to retrieve passwords, and then invoke {@link #close()} on the instance when done, 47 * enabling the instance to release any held resources. 48 */ 49 public 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 PasswordRetriever()59 public 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 */ getPasswords( String spec, String description, Charset... additionalPwdEncodings)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 */ getPasswords(char[] pwd, Charset... additionalEncodings)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 */ getPasswords( byte[] encodedPwd, Charset encodingForDecoding, Charset... additionalEncodings)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 */ addPasswords(List<char[]> passwords, char[] pwd, Charset... additionalEncodings)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 */ addPassword(List<char[]> passwords, char[] password)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 encodePassword(char[] pwd, Charset cs)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 decodePassword(byte[] pwdBytes, Charset encoding)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 */ castBytesToChars(byte[] bytes)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 isJava9OrHigherErrOnTheSideOfCaution()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 */ getConsoleEncoding()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 getCharsetByName(String charsetName)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 readEncodedPassword(InputStream in)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 assertNotClosed()390 private void assertNotClosed() { 391 if (mClosed) { 392 throw new IllegalStateException("Closed"); 393 } 394 } 395 396 @Override close()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