001/* 002 * Copyright 2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2019 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.util; 022 023 024 025import java.io.BufferedReader; 026import java.io.File; 027import java.io.FileInputStream; 028import java.io.InputStream; 029import java.io.IOException; 030import java.io.InputStreamReader; 031import java.io.PrintStream; 032import java.security.GeneralSecurityException; 033import java.util.ArrayList; 034import java.util.Arrays; 035import java.util.Collections; 036import java.util.List; 037import java.util.concurrent.CopyOnWriteArrayList; 038 039import com.unboundid.ldap.sdk.LDAPException; 040import com.unboundid.ldap.sdk.ResultCode; 041import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils; 042 043import static com.unboundid.util.UtilityMessages.*; 044 045 046 047/** 048 * This class provides a mechanism for reading a password from a file. Password 049 * files must contain exactly one line, which must be non-empty, and the entire 050 * content of that line will be used as the password. 051 * <BR><BR> 052 * The contents of the file may have optionally been encrypted with the 053 * {@link PassphraseEncryptedOutputStream}, and may have optionally been 054 * compressed with the {@code GZIPOutputStream}. If the data is both compressed 055 * and encrypted, then it must have been compressed before it was encrypted, so 056 * that it is necessary to decrypt the data before it can be decompressed. 057 * <BR><BR> 058 * If the file is encrypted, then the encryption key may be obtained in one of 059 * the following ways: 060 * <UL> 061 * <LI>If this code is running in a tool that is part of a Ping Identity 062 * Directory Server installation (or a related product like the Directory 063 * Proxy Server or Data Synchronization Server, or an alternately branded 064 * version of these products, like the Alcatel-Lucent or Nokia 8661 065 * versions), and the file was encrypted with a key from that server's 066 * encryption settings database, then the tool will try to get the 067 * key from the corresponding encryption settings definition. In many 068 * cases, this may not require any interaction from the user at all.</LI> 069 * <LI>The reader maintains a cache of passwords that have been previously 070 * used. If the same password is used to encrypt multiple files, it may 071 * only need to be requested once from the user. The caller can also 072 * manually add passwords to this cache if they are known in advance.</LI> 073 * <LI>The user can be interactively prompted for the password.</LI> 074 * </UL> 075 */ 076@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 077public final class PasswordFileReader 078{ 079 // A list of passwords that will be tried as encryption keys if an encrypted 080 // password file is encountered. 081 private final CopyOnWriteArrayList<char[]> encryptionPasswordCache; 082 083 // The print stream that should be used as standard output of an encrypted 084 // password file is encountered and it is necessary to prompt for the password 085 // used as the encryption key. 086 private final PrintStream standardError; 087 088 // The print stream that should be used as standard output of an encrypted 089 // password file is encountered and it is necessary to prompt for the password 090 // used as the encryption key. 091 private final PrintStream standardOutput; 092 093 094 095 /** 096 * Creates a new instance of this password file reader. The JVM-default 097 * standard output and error streams will be used. 098 */ 099 public PasswordFileReader() 100 { 101 this(System.out, System.err); 102 } 103 104 105 106 /** 107 * Creates a new instance of this password file reader. 108 * 109 * @param standardOutput The print stream that should be used as standard 110 * output if an encrypted password file is encountered 111 * and it is necessary to prompt for the password 112 * used as the encryption key. This must not be 113 * {@code null}. 114 * @param standardError The print stream that should be used as standard 115 * error if an encrypted password file is encountered 116 * and it is necessary to prompt for the password 117 * used as the encryption key. This must not be 118 * {@code null}. 119 */ 120 public PasswordFileReader(final PrintStream standardOutput, 121 final PrintStream standardError) 122 { 123 Validator.ensureNotNullWithMessage(standardOutput, 124 "PasswordFileReader.standardOutput must not be null."); 125 Validator.ensureNotNullWithMessage(standardError, 126 "PasswordFileReader.standardError must not be null."); 127 128 this.standardOutput = standardOutput; 129 this.standardError = standardError; 130 131 encryptionPasswordCache = new CopyOnWriteArrayList<>(); 132 } 133 134 135 136 /** 137 * Attempts to read a password from the specified file. 138 * 139 * @param path The path to the file from which the password should be read. 140 * It must not be {@code null}, and the file must exist. 141 * 142 * @return The characters that comprise the password read from the specified 143 * file. 144 * 145 * @throws IOException If a problem is encountered while trying to read the 146 * password from the file. 147 * 148 * @throws LDAPException If the file does not exist, if it does not contain 149 * exactly one line, or if that line is empty. 150 */ 151 public char[] readPassword(final String path) 152 throws IOException, LDAPException 153 { 154 return readPassword(new File(path)); 155 } 156 157 158 159 /** 160 * Attempts to read a password from the specified file. 161 * 162 * @param file The path file from which the password should be read. It 163 * must not be {@code null}, and the file must exist. 164 * 165 * @return The characters that comprise the password read from the specified 166 * file. 167 * 168 * @throws IOException If a problem is encountered while trying to read the 169 * password from the file. 170 * 171 * @throws LDAPException If the file does not exist, if it does not contain 172 * exactly one line, or if that line is empty. 173 */ 174 public char[] readPassword(final File file) 175 throws IOException, LDAPException 176 { 177 if (! file.exists()) 178 { 179 throw new IOException(ERR_PW_FILE_READER_FILE_MISSING.get( 180 file.getAbsolutePath())); 181 } 182 183 if (! file.isFile()) 184 { 185 throw new IOException(ERR_PW_FILE_READER_FILE_NOT_FILE.get( 186 file.getAbsolutePath())); 187 } 188 189 InputStream inputStream = new FileInputStream(file); 190 try 191 { 192 try 193 { 194 final ObjectPair<InputStream, char[]> encryptedFileData = 195 ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream, 196 encryptionPasswordCache, true, 197 INFO_PW_FILE_READER_ENTER_PW_PROMPT 198 .get(file.getAbsolutePath()), 199 ERR_PW_FILE_READER_WRONG_PW.get(file.getAbsolutePath()), 200 standardOutput, standardError); 201 inputStream = encryptedFileData.getFirst(); 202 203 final char[] encryptionPassword = encryptedFileData.getSecond(); 204 if (encryptionPassword != null) 205 { 206 synchronized (encryptionPasswordCache) 207 { 208 boolean passwordIsAlreadyCached = false; 209 for (final char[] cachedPassword : encryptionPasswordCache) 210 { 211 if (Arrays.equals(encryptionPassword, cachedPassword)) 212 { 213 passwordIsAlreadyCached = true; 214 break; 215 } 216 } 217 218 if (!passwordIsAlreadyCached) 219 { 220 encryptionPasswordCache.add(encryptionPassword); 221 } 222 } 223 } 224 } 225 catch (final GeneralSecurityException e) 226 { 227 Debug.debugException(e); 228 throw new IOException(e); 229 } 230 231 inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream); 232 233 try (BufferedReader reader = 234 new BufferedReader(new InputStreamReader(inputStream))) 235 { 236 final String passwordLine = reader.readLine(); 237 if (passwordLine == null) 238 { 239 throw new LDAPException(ResultCode.PARAM_ERROR, 240 ERR_PW_FILE_READER_FILE_EMPTY.get(file.getAbsolutePath())); 241 } 242 243 final String secondLine = reader.readLine(); 244 if (secondLine != null) 245 { 246 throw new LDAPException(ResultCode.PARAM_ERROR, 247 ERR_PW_FILE_READER_FILE_HAS_MULTIPLE_LINES.get( 248 file.getAbsolutePath())); 249 } 250 251 if (passwordLine.isEmpty()) 252 { 253 throw new LDAPException(ResultCode.PARAM_ERROR, 254 ERR_PW_FILE_READER_FILE_HAS_EMPTY_LINE.get( 255 file.getAbsolutePath())); 256 } 257 258 return passwordLine.toCharArray(); 259 } 260 } 261 finally 262 { 263 try 264 { 265 266 inputStream.close(); 267 } 268 catch (final Exception e) 269 { 270 Debug.debugException(e); 271 } 272 } 273 } 274 275 276 277 /** 278 * Retrieves a list of the encryption passwords currently held in the cache. 279 * 280 * @return A list of the encryption passwords currently held in the cache, or 281 * an empty list if there are no cached passwords. 282 */ 283 public List<char[]> getCachedEncryptionPasswords() 284 { 285 final ArrayList<char[]> cacheCopy; 286 synchronized (encryptionPasswordCache) 287 { 288 cacheCopy = new ArrayList<>(encryptionPasswordCache.size()); 289 for (final char[] cachedPassword : encryptionPasswordCache) 290 { 291 cacheCopy.add(Arrays.copyOf(cachedPassword, cachedPassword.length)); 292 } 293 } 294 295 return Collections.unmodifiableList(cacheCopy); 296 } 297 298 299 300 /** 301 * Adds the provided password to the cache of passwords that will be tried as 302 * potential encryption keys if an encrypted password file is encountered. 303 * 304 * @param encryptionPassword A password to add to the cache of passwords 305 * that will be tried as potential encryption keys 306 * if an encrypted password file is encountered. 307 * It must not be {@code null} or empty. 308 */ 309 public void addToEncryptionPasswordCache(final String encryptionPassword) 310 { 311 addToEncryptionPasswordCache(encryptionPassword.toCharArray()); 312 } 313 314 315 316 /** 317 * Adds the provided password to the cache of passwords that will be tried as 318 * potential encryption keys if an encrypted password file is encountered. 319 * 320 * @param encryptionPassword A password to add to the cache of passwords 321 * that will be tried as potential encryption keys 322 * if an encrypted password file is encountered. 323 * It must not be {@code null} or empty. 324 */ 325 public void addToEncryptionPasswordCache(final char[] encryptionPassword) 326 { 327 Validator.ensureNotNullWithMessage(encryptionPassword, 328 "PasswordFileReader.addToEncryptionPasswordCache.encryptionPassword " + 329 "must not be null or empty."); 330 Validator.ensureTrue((encryptionPassword.length > 0), 331 "PasswordFileReader.addToEncryptionPasswordCache.encryptionPassword " + 332 "must not be null or empty."); 333 334 synchronized (encryptionPasswordCache) 335 { 336 for (final char[] cachedPassword : encryptionPasswordCache) 337 { 338 if (Arrays.equals(cachedPassword, encryptionPassword)) 339 { 340 return; 341 } 342 } 343 344 encryptionPasswordCache.add(encryptionPassword); 345 } 346 } 347 348 349 350 /** 351 * Clears the cache of passwords that will be tried as potential encryption 352 * keys if an encrypted password file is encountered. 353 * 354 * @param zeroArrays Indicates whether to zero out the contents of the 355 * cached passwords before clearing them. If this is 356 * {@code true}, then all of the backing arrays for the 357 * cached passwords will be overwritten with all null 358 * characters to erase the original passwords from memory. 359 */ 360 public void clearEncryptionPasswordCache(final boolean zeroArrays) 361 { 362 synchronized (encryptionPasswordCache) 363 { 364 if (zeroArrays) 365 { 366 for (final char[] cachedPassword : encryptionPasswordCache) 367 { 368 Arrays.fill(cachedPassword, '\u0000'); 369 } 370 } 371 372 encryptionPasswordCache.clear(); 373 } 374 } 375}