content: /*
   * Copyright 2009-2018, Acciente LLC
   *
   * Acciente LLC licenses this file to you under the
   * Apache License, Version 2.0 (the "License"); you
   * may not use this file except in compliance with the
   * License. You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in
   * writing, software distributed under the License is
   * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
   * OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing
   * permissions and limitations under the License.
   */
  
  package com.acciente.oacc.encryptor.jasypt;
  
  import org.jasypt.contrib.org.apache.commons.codec_1_3.binary.Base64;
  
  import java.nio.charset.StandardCharsets;
  import java.util.regex.Pattern;
  
  class PasswordEncoderDecoder {
     private static final String MARKER                 = JasyptPasswordEncryptor.NAME + ":";
     private static final String PARAM_DELIMITER        = "$";
     private static final String QUOTED_PARAM_DELIMITER = Pattern.quote(PARAM_DELIMITER);
  
     private static final int DECODED_PASSWORD_ARRAY_ALGORITHM       = 0;
     private static final int DECODED_PASSWORD_ARRAY_ITERATIONS      = 1;
     private static final int DECODED_PASSWORD_ARRAY_SALT_SIZE_BYTES = 2;
     private static final int DECODED_PASSWORD_ARRAY_DIGEST          = 3;
     private static final int DECODED_PASSWORD_ARRAY_COUNT           = 4;
  
     private static final Base64 base64 = new Base64();
  
     /**
      * Encodes an identifying header, the parameters used to generate the Jasypt digest, and the Jasypt digest into a
      * single "self-contained" password that contains enough information for future comparisons.
      *
      * @param algorithm     the algorithm used to generate the digest.
      * @param iterations    the number of iterations used to generate the digest.
      * @param saltSizeBytes the salt size bytes used to generate the digest.
      * @param digest        the digest itself.
      * @return a fully-encoded password ready for persistent storage.
      */
     String encode(String algorithm,
                   int iterations,
                   int saltSizeBytes,
                   byte[] digest) {
        // setup array of values to encode -- to help ensure same param sequence in encode and decode logic
        final Object[] decodedPasswordArray = new Object[DECODED_PASSWORD_ARRAY_COUNT];
        decodedPasswordArray[DECODED_PASSWORD_ARRAY_ALGORITHM] = algorithm;
        decodedPasswordArray[DECODED_PASSWORD_ARRAY_ITERATIONS] = iterations;
        decodedPasswordArray[DECODED_PASSWORD_ARRAY_SALT_SIZE_BYTES] = saltSizeBytes;
        decodedPasswordArray[DECODED_PASSWORD_ARRAY_DIGEST] = new String(base64.encode(digest), StandardCharsets.US_ASCII);
  
        final StringBuilder encodedPassword = new StringBuilder(128).append(MARKER);
        for (int i = 0; i < DECODED_PASSWORD_ARRAY_COUNT - 1; i++) {
           encodedPassword.append(decodedPasswordArray[i]);
           encodedPassword.append(PARAM_DELIMITER);
        }
        // encode last param without the delimiter
        encodedPassword.append(decodedPasswordArray[DECODED_PASSWORD_ARRAY_COUNT - 1]);
  
        return encodedPassword.toString();
     }
  
     /**
      * Decodes a previously encoded Jasypt password into its constituent parts (algorithm, iterations, salt size, digest).
      *
      * @param encodedPassword an encoded password that was previously returned by
      *                        {{@link #encode(String, int, int, byte[])}}.
      * @return an object containing the decoded parts of the password (algorithm, iterations, salt size, digest).
      */
     DecodedPassword decode(String encodedPassword) {
        if (encodedPassword.startsWith(MARKER)) {
           final String[] decodedPasswordArray = encodedPassword.substring(MARKER.length()).split(QUOTED_PARAM_DELIMITER);
  
           if (decodedPasswordArray.length != DECODED_PASSWORD_ARRAY_COUNT) {
              throw new IllegalArgumentException("Unexpected format for Jasypt password: " +
                                                       encodedPassword.substring(0, MARKER.length()));
           }
  
           final String algorithm;
           final int    iterations;
           final int    saltSizeBytes;
           final byte[] digest;
  
           algorithm = decodedPasswordArray[DECODED_PASSWORD_ARRAY_ALGORITHM];
           try {
              iterations = Integer.parseInt(decodedPasswordArray[DECODED_PASSWORD_ARRAY_ITERATIONS]);
              saltSizeBytes = Integer.parseInt(decodedPasswordArray[DECODED_PASSWORD_ARRAY_SALT_SIZE_BYTES]);
           }
           catch (NumberFormatException e) {
              throw new IllegalArgumentException("Unexpected value in Jasypt password header for iterations and/or salt size: " + encodedPassword);
           }
           digest = base64.decode(decodedPasswordArray[DECODED_PASSWORD_ARRAY_DIGEST].getBytes(StandardCharsets.US_ASCII));
  
           return new DecodedPassword.Builder()
                 .algorithm(algorithm)
                 .iterations(iterations)
                 .saltSizeBytes(saltSizeBytes)
                 .digest(digest)
                 .build();
        }
        else {
           throw new IllegalArgumentException("Unexpected marker for Jasypt password: " +
                                                    encodedPassword.substring(0, MARKER.length()));
        }
     }
  }
  