/*
 * Licensed to Elasticsearch B.V. under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch B.V. 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 org.logstash.secret.store;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Base64;
import java.util.Random;

/**
 * Conversion utility between String, bytes, and chars. All methods attempt to keep sensitive data out of memory. Sensitive data should avoid using Java {@link String}'s.
 */
final public class SecretStoreUtil {

    /**
     * Private constructor. Utility class.
     */
    private SecretStoreUtil() {
    }

    private static final Random RANDOM = new Random();

    /**
     * Converts bytes from ascii encoded text to a char[] and zero outs the original byte[]
     *
     * @param bytes the bytes from an ascii encoded text (note - no validation is done to ensure ascii encoding)
     * @return the corresponding char[]
     */
    public static char[] asciiBytesToChar(byte[] bytes) {
        char[] chars = new char[bytes.length];
        for (int i = 0; i < bytes.length; i++) {
            chars[i] = (char) bytes[i];
            bytes[i] = '\0';
        }
        return chars;
    }

    /**
     * Converts characters from ascii encoded text to a byte[] and zero outs the original char[]
     *
     * @param chars the chars from an ascii encoded text (note - no validation is done to ensure ascii encoding)
     * @return the corresponding byte[]
     */
    public static byte[] asciiCharToBytes(char[] chars) {
        byte[] bytes = new byte[chars.length];
        for (int i = 0; i < chars.length; i++) {
            bytes[i] = (byte) chars[i];
            chars[i] = '\0';
        }
        return bytes;
    }

    /**
     * Base64 encode the given byte[], then zero the original byte[]
     *
     * @param b the byte[] to base64 encode
     * @return the base64 encoded bytes
     */
    public static byte[] base64Encode(byte[] b) {
        byte[] bytes = Base64.getEncoder().encode(b);
        clearBytes(b);
        return bytes;
    }

    /**
     * Base64 encode the given byte[], then zero out the original byte[]
     *
     * @param bytes the byte[] to base64 encode
     * @return the char[] representation of the base64 encoding
     */
    public static char[] base64EncodeToChars(byte[] bytes) {
        return asciiBytesToChar(base64Encode(bytes));
    }

    /**
     * Base64 encode the given char[], then zero out the original char[]
     *
     * @param chars the char[] to base64 encode
     * @return the char[] representation of the base64 encoding
     */
    public static char[] base64Encode(char[] chars) {
        return asciiBytesToChar(base64Encode(asciiCharToBytes(chars)));
    }

    /**
     * Decodes a Base64 encoded byte[], then zero out the original byte[]
     *
     * @param b the base64 bytes
     * @return the non-base64 encoded bytes
     */
    public static byte[] base64Decode(byte[] b) {
        byte[] bytes = Base64.getDecoder().decode(b);
        clearBytes(b);
        return bytes;
    }

    /**
     * Decodes a Base64 encoded char[], then zero out the original char[]
     *
     * @param chars the base64 chars
     * @return the non-base64 encoded chars
     */
    public static byte[] base64Decode(char[] chars) {
        return base64Decode(asciiCharToBytes(chars));
    }

    /**
     * Attempt to keep data out of the heap.
     *
     * @param chars the bytes to zero out
     */
    public static void clearChars(char[] chars) {
        Arrays.fill(chars, '\0');
    }

    /**
     * Attempt to keep data out of the heap.
     *
     * @param bytes the bytes to zero out
     */
    public static void clearBytes(byte[] bytes) {
        Arrays.fill(bytes, (byte) '\0');
    }


    /**
     * De-obfuscates the obfuscated char[] generated by {@link SecretStoreUtil#obfuscate(char[])}
     *
     * @param chars The chars to de-obscure
     * @return the de-obscured chars
     */
    public static char[] deObfuscate(char[] chars) {
        byte[] bytes = asciiCharToBytes(chars);
        byte[] random = Arrays.copyOfRange(bytes, bytes.length / 2, bytes.length);
        byte[] deObfuscated = new byte[random.length];
        for (int i = 0; i < random.length; i++) {
            int xor = bytes[i] ^ random[i];
            deObfuscated[i] = ((byte) (xor & 0xff));
        }
        return asciiBytesToChar(deObfuscated);
    }

    /**
     * <p>Simple obfuscation that adds a bit of randomness and shuffles the bits of a char[].</p>
     * <p>Note - this is NOT security and will only deter the lazy.</p>
     *
     * @param chars The chars to obscure
     * @return the obscured bytes
     */
    public static char[] obfuscate(char[] chars) {
        byte[] bytes = asciiCharToBytes(chars);
        byte[] random = new byte[bytes.length];
        RANDOM.nextBytes(random);

        ByteBuffer obfuscated = ByteBuffer.allocate(bytes.length * 2);
        for (int i = 0; i < bytes.length; i++) {
            int xor = bytes[i] ^ random[i];
            obfuscated.put((byte) (0xff & xor));
        }
        obfuscated.put(random);
        char[] result = asciiBytesToChar(obfuscated.array());
        clearBytes(obfuscated.array());
        return result;
    }
}
