001/* 002 * Copyright 2019-2020 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2019-2020 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-2020 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 // A list of passwords that will be tried as encryption keys if an encrypted 095 // password file is encountered. 096 @NotNull private final CopyOnWriteArrayList<char[]> encryptionPasswordCache; 097 098 // The print stream that should be used as standard output of an encrypted 099 // password file is encountered and it is necessary to prompt for the password 100 // used as the encryption key. 101 @NotNull private final PrintStream standardError; 102 103 // The print stream that should be used as standard output of an encrypted 104 // password file is encountered and it is necessary to prompt for the password 105 // used as the encryption key. 106 @NotNull private final PrintStream standardOutput; 107 108 109 110 /** 111 * Creates a new instance of this password file reader. The JVM-default 112 * standard output and error streams will be used. 113 */ 114 public PasswordFileReader() 115 { 116 this(System.out, System.err); 117 } 118 119 120 121 /** 122 * Creates a new instance of this password file reader. 123 * 124 * @param standardOutput The print stream that should be used as standard 125 * output if an encrypted password file is encountered 126 * and it is necessary to prompt for the password 127 * used as the encryption key. This must not be 128 * {@code null}. 129 * @param standardError The print stream that should be used as standard 130 * error if an encrypted password file is encountered 131 * and it is necessary to prompt for the password 132 * used as the encryption key. This must not be 133 * {@code null}. 134 */ 135 public PasswordFileReader(@NotNull final PrintStream standardOutput, 136 @NotNull final PrintStream standardError) 137 { 138 Validator.ensureNotNullWithMessage(standardOutput, 139 "PasswordFileReader.standardOutput must not be null."); 140 Validator.ensureNotNullWithMessage(standardError, 141 "PasswordFileReader.standardError must not be null."); 142 143 this.standardOutput = standardOutput; 144 this.standardError = standardError; 145 146 encryptionPasswordCache = new CopyOnWriteArrayList<>(); 147 } 148 149 150 151 /** 152 * Attempts to read a password from the specified file. 153 * 154 * @param path The path to the file from which the password should be read. 155 * It must not be {@code null}, and the file must exist. 156 * 157 * @return The characters that comprise the password read from the specified 158 * file. 159 * 160 * @throws IOException If a problem is encountered while trying to read the 161 * password from the file. 162 * 163 * @throws LDAPException If the file does not exist, if it does not contain 164 * exactly one line, or if that line is empty. 165 */ 166 @NotNull() 167 public char[] readPassword(@NotNull final String path) 168 throws IOException, LDAPException 169 { 170 return readPassword(new File(path)); 171 } 172 173 174 175 /** 176 * Attempts to read a password from the specified file. 177 * 178 * @param file The path file from which the password should be read. It 179 * must not be {@code null}, and the file must exist. 180 * 181 * @return The characters that comprise the password read from the specified 182 * file. 183 * 184 * @throws IOException If a problem is encountered while trying to read the 185 * password from the file. 186 * 187 * @throws LDAPException If the file does not exist, if it does not contain 188 * exactly one line, or if that line is empty. 189 */ 190 @NotNull() 191 public char[] readPassword(@NotNull final File file) 192 throws IOException, LDAPException 193 { 194 if (! file.exists()) 195 { 196 throw new IOException(ERR_PW_FILE_READER_FILE_MISSING.get( 197 file.getAbsolutePath())); 198 } 199 200 if (! file.isFile()) 201 { 202 throw new IOException(ERR_PW_FILE_READER_FILE_NOT_FILE.get( 203 file.getAbsolutePath())); 204 } 205 206 InputStream inputStream = new FileInputStream(file); 207 try 208 { 209 try 210 { 211 final ObjectPair<InputStream, char[]> encryptedFileData = 212 ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream, 213 encryptionPasswordCache, true, 214 INFO_PW_FILE_READER_ENTER_PW_PROMPT 215 .get(file.getAbsolutePath()), 216 ERR_PW_FILE_READER_WRONG_PW.get(file.getAbsolutePath()), 217 standardOutput, standardError); 218 inputStream = encryptedFileData.getFirst(); 219 220 final char[] encryptionPassword = encryptedFileData.getSecond(); 221 if (encryptionPassword != null) 222 { 223 synchronized (encryptionPasswordCache) 224 { 225 boolean passwordIsAlreadyCached = false; 226 for (final char[] cachedPassword : encryptionPasswordCache) 227 { 228 if (Arrays.equals(encryptionPassword, cachedPassword)) 229 { 230 passwordIsAlreadyCached = true; 231 break; 232 } 233 } 234 235 if (!passwordIsAlreadyCached) 236 { 237 encryptionPasswordCache.add(encryptionPassword); 238 } 239 } 240 } 241 } 242 catch (final GeneralSecurityException e) 243 { 244 Debug.debugException(e); 245 throw new IOException(e); 246 } 247 248 inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream); 249 250 try (BufferedReader reader = 251 new BufferedReader(new InputStreamReader(inputStream))) 252 { 253 final String passwordLine = reader.readLine(); 254 if (passwordLine == null) 255 { 256 throw new LDAPException(ResultCode.PARAM_ERROR, 257 ERR_PW_FILE_READER_FILE_EMPTY.get(file.getAbsolutePath())); 258 } 259 260 final String secondLine = reader.readLine(); 261 if (secondLine != null) 262 { 263 throw new LDAPException(ResultCode.PARAM_ERROR, 264 ERR_PW_FILE_READER_FILE_HAS_MULTIPLE_LINES.get( 265 file.getAbsolutePath())); 266 } 267 268 if (passwordLine.isEmpty()) 269 { 270 throw new LDAPException(ResultCode.PARAM_ERROR, 271 ERR_PW_FILE_READER_FILE_HAS_EMPTY_LINE.get( 272 file.getAbsolutePath())); 273 } 274 275 return passwordLine.toCharArray(); 276 } 277 } 278 finally 279 { 280 try 281 { 282 283 inputStream.close(); 284 } 285 catch (final Exception e) 286 { 287 Debug.debugException(e); 288 } 289 } 290 } 291 292 293 294 /** 295 * Retrieves a list of the encryption passwords currently held in the cache. 296 * 297 * @return A list of the encryption passwords currently held in the cache, or 298 * an empty list if there are no cached passwords. 299 */ 300 @NotNull() 301 public List<char[]> getCachedEncryptionPasswords() 302 { 303 final ArrayList<char[]> cacheCopy; 304 synchronized (encryptionPasswordCache) 305 { 306 cacheCopy = new ArrayList<>(encryptionPasswordCache.size()); 307 for (final char[] cachedPassword : encryptionPasswordCache) 308 { 309 cacheCopy.add(Arrays.copyOf(cachedPassword, cachedPassword.length)); 310 } 311 } 312 313 return Collections.unmodifiableList(cacheCopy); 314 } 315 316 317 318 /** 319 * Adds the provided password to the cache of passwords that will be tried as 320 * potential encryption keys if an encrypted password file is encountered. 321 * 322 * @param encryptionPassword A password to add to the cache of passwords 323 * that will be tried as potential encryption keys 324 * if an encrypted password file is encountered. 325 * It must not be {@code null} or empty. 326 */ 327 public void addToEncryptionPasswordCache( 328 @NotNull final String encryptionPassword) 329 { 330 addToEncryptionPasswordCache(encryptionPassword.toCharArray()); 331 } 332 333 334 335 /** 336 * Adds the provided password to the cache of passwords that will be tried as 337 * potential encryption keys if an encrypted password file is encountered. 338 * 339 * @param encryptionPassword A password to add to the cache of passwords 340 * that will be tried as potential encryption keys 341 * if an encrypted password file is encountered. 342 * It must not be {@code null} or empty. 343 */ 344 public void addToEncryptionPasswordCache( 345 @NotNull final char[] encryptionPassword) 346 { 347 Validator.ensureNotNullWithMessage(encryptionPassword, 348 "PasswordFileReader.addToEncryptionPasswordCache.encryptionPassword " + 349 "must not be null or empty."); 350 Validator.ensureTrue((encryptionPassword.length > 0), 351 "PasswordFileReader.addToEncryptionPasswordCache.encryptionPassword " + 352 "must not be null or empty."); 353 354 synchronized (encryptionPasswordCache) 355 { 356 for (final char[] cachedPassword : encryptionPasswordCache) 357 { 358 if (Arrays.equals(cachedPassword, encryptionPassword)) 359 { 360 return; 361 } 362 } 363 364 encryptionPasswordCache.add(encryptionPassword); 365 } 366 } 367 368 369 370 /** 371 * Clears the cache of passwords that will be tried as potential encryption 372 * keys if an encrypted password file is encountered. 373 * 374 * @param zeroArrays Indicates whether to zero out the contents of the 375 * cached passwords before clearing them. If this is 376 * {@code true}, then all of the backing arrays for the 377 * cached passwords will be overwritten with all null 378 * characters to erase the original passwords from memory. 379 */ 380 public void clearEncryptionPasswordCache(final boolean zeroArrays) 381 { 382 synchronized (encryptionPasswordCache) 383 { 384 if (zeroArrays) 385 { 386 for (final char[] cachedPassword : encryptionPasswordCache) 387 { 388 Arrays.fill(cachedPassword, '\u0000'); 389 } 390 } 391 392 encryptionPasswordCache.clear(); 393 } 394 } 395}