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