/*
 * Written by Dawid Kurzyniec and released to the public domain, as explained
 * at http://creativecommons.org/licenses/publicdomain
 */

package edu.emory.mathcs.util.security;

import java.util.*;
import java.io.*;

/**
 * Utility methods to securely manipulate on character arrays. The methods
 * allow to treat the character arrays similarly to strings, yet they ensure
 * that all temporary arrays are zeroed-out before discarding.
 * <p>
 * Application of this class stems from the fact that String class is not
 * appropriate for holding passwords and other sensitive information.
 * Strings cannot be zeroed-out before unreferencing, thus the content
 * may be dangling in memory for quite a while before it is garbage-collected,
 * and it can stay in the process data block even longer, until it is
 * overwritten. It has been demonstrated that attacker can obtain the data
 * in clear text by forcing the operating system to swap out the application
 * in question, and then by reading the swap file.
 * To minimize the risk, sensitive data should be cleared explicitly as soon as
 * possible. It suggests using mutable character arrays in favor of strings.
 * This class allows to operate on such arrays much like on strings, in
 * particular, it provides methods to securely concatenate them, as well as
 * write them to, and read them from streams in the UTF format.
 *
 * @author Dawid Kurzyniec
 * @version 1.0
 */

public class CharStrings {
    /** this is an utility class */
    private CharStrings() {}

    /**
     * Returns a concatenation of two character arrays.
     * @param s1 first array
     * @param s2 second array
     * @return new array containing s1 concatenated with s2
     */
    public static char[] concat(char[] s1, char[] s2) {
        if (s1 == null) return s2;
        if (s2 == null) return s1;
        char[] s3 = new char[s1.length + s2.length];
        System.arraycopy(s1, 0, s3, 0, s1.length);
        System.arraycopy(s2, 0, s3, s1.length, s2.length);
        return s3;
    }

    /**
     * Compares two character arrays.
     *
     * @param s1 first array
     * @param s2 second array
     * @return true if arrays have identical content; false otherwise
     */
    public static boolean equals(char[] s1, char[] s2) {
        return Arrays.equals(s1, s2);
    }

    /**
     * Zeroes-out the specified character array.
     * @param s the array to zero-out
     */
    public static void clear(char[] s) {
        if (s != null) Arrays.fill(s, (char)0);
    }

    /**
     * Writes the specified character array to the output stream using UTF-8
     * encoding.
     *
     * @param out the output to write to
     * @param s array to write to
     * @return the number of bytes written
     * @throws IOException if I/O error occurs
     */
    public static int writeUTF(OutputStream out, char[] s) throws IOException {
        return writeUTF(out, s, 0, s.length);
    }

    /**
     * Writes a portion of the specified character array to the output stream
     * using UTF-8 encoding.
     *
     * @param out the output to write to
     * @param s array to write to
     * @param off start offset within s
     * @param len number of characters to write
     * @return the number of bytes written
     * @throws IOException if I/O error occurs
     */
    public static int writeUTF(OutputStream out, char[] s, int off, int len)
        throws IOException
    {
        int utflen = getUTFLen(s, off, len);
        if (utflen > 65535) {
            throw new UTFDataFormatException();
        }
        byte[] dest = new byte[utflen+2];
        dest[0] = (byte) ((utflen >>> 8) & 0xFF);
        dest[1] = (byte) ((utflen >>> 0) & 0xFF);
        toUTF(s, off, len, dest, 2);
        out.write(dest);
        Arrays.fill(dest, (byte)0);
        return dest.length;
    }

    /**
     * Converts the specified character array into its UTF-8 encoding.
     *
     * @param s the array to encode
     * @return UTF-8 encoded array
     */
    public static byte[] toUTF(char[] s) {
        return toUTF(s, 0, s.length);
    }

    /**
     * Converts a portion of the specified character array into its UTF-8
     * encoding.
     *
     * @param s the array to encode
     * @param off the start offset within s
     * @param len the number of characters to encode
     * @return UTF-8 encoded array
     */
    public static byte[] toUTF(char[] s, int off, int len) {
        byte[] dest = new byte[getUTFLen(s, off, len)];
        toUTF(s, off, len, dest, 0);
        return dest;
    }

    /**
     * Returns the number of bytes of an UTF-8 encoding of a portion of the
     * specified character array.
     *
     * @param s the character array
     * @param off the start offset within s
     * @param len the number of characters to include
     * @return the number of bytes of an UTF-8 encoding
     */
    public static int getUTFLen(char[] s, int off, int len) {
        if (s == null) {
            throw new NullPointerException();
        } else if ((off < 0) || (off > s.length) || (len < 0) ||
                   ((off + len) > s.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        }

        int utflen = 0;
        int c;

        for (int i = 0; i < len; i++) {
            c = s[i];
            if ((c >= 0x0001) && (c <= 0x007F)) {
                utflen++;
            } else if (c > 0x07FF) {
                utflen += 3;
            } else {
                utflen += 2;
            }
        }

        return utflen;
    }

    private static void toUTF(char[] s, int off, int len, byte[] dest, int destoff) {
        int c, count = destoff;

        for (int i = 0; i < len; i++) {
            c = s[i];
            if ((c >= 0x0001) && (c <= 0x007F)) {
                dest[count++] = (byte) c;
            } else if (c > 0x07FF) {
                dest[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
                dest[count++] = (byte) (0x80 | ((c >>  6) & 0x3F));
                dest[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
            } else {
                dest[count++] = (byte) (0xC0 | ((c >>  6) & 0x1F));
                dest[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
            }
        }
    }

    /**
     * Reads UTF-8 encoded character array from an input stream.
     * @param in the input stream to read from
     * @return decoded character array
     * @throws IOException if I/O error occurs
     */
    public final static char[] readUTF(InputStream in) throws IOException {
        DataInput din = (in instanceof DataInput)
            ? (DataInput)in
            : new DataInputStream(in);
        return readUTF(din);
    }

    /**
     * Reads UTF-8 encoded character array from a data input.
     * @param in the data input to read from
     * @return decoded character array
     * @throws IOException if I/O error occurs
     */
    public final static char[] readUTF(DataInput in) throws IOException {
        int utflen = in.readUnsignedShort();
        byte bytearr[] = new byte[utflen];
        try {
            in.readFully(bytearr, 0, utflen);
            return fromUTF(bytearr);
        }
        finally {
            Arrays.fill(bytearr, (byte)0);
        }
    }

    /**
     * Recovers a character array out of its UTF-8 encoding.
     *
     * @param utfString containing UTF-8-encoded character array
     * @return the decoded character array
     * @throws IOException if I/O error occurs
     */
    public final static char[] fromUTF(final byte[] utfString) throws IOException {
        return fromUTF(utfString, 0, utfString.length);
    }

    /**
     * Recovers a character array out of its UTF-8 encoding.
     *
     * @param utfString array containing UTF-8-encoded character array
     * @param off start offset within utfString
     * @param len number of bytes to include
     * @return the decoded character array
     * @throws IOException if I/O error occurs
     */
    public final static char[] fromUTF(final byte[] utfString, int off, int len) throws IOException {
        char[] arr = new char[len];
        int c, char2, char3;
        int idx = 0;

        try {
            while (off < len) {
                c = (int) utfString[off] & 0xff;
                switch (c >> 4) {
                    case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                        /* 0xxxxxxx*/
                        off++;
                        arr[idx++] = (char) c;
                        break;
                    case 12: case 13:
                        /* 110x xxxx   10xx xxxx*/
                        off += 2;
                        if (off > len)
                            throw new UTFDataFormatException();
                        char2 = (int) utfString[off - 1];
                        if ( (char2 & 0xC0) != 0x80)
                            throw new UTFDataFormatException();
                        arr[idx++] = (char) ( ( (c & 0x1F) << 6) |
                                             (char2 & 0x3F));
                        break;
                    case 14:
                        /* 1110 xxxx  10xx xxxx  10xx xxxx */
                        off += 3;
                        if (off > len)
                            throw new UTFDataFormatException();
                        char2 = (int) utfString[off - 2];
                        char3 = (int) utfString[off - 1];
                        if ( ( (char2 & 0xC0) != 0x80) ||
                            ( (char3 & 0xC0) != 0x80))
                            throw new UTFDataFormatException();
                        arr[idx++] = (char) ( ( (c & 0x0F) << 12) |
                                             ( (char2 & 0x3F) << 6) |
                                             ( (char3 & 0x3F) << 0));
                        break;
                    default:
                        /* 10xx xxxx,  1111 xxxx */
                        throw new UTFDataFormatException();
                }
            }
            // The number of chars produced may be less than utflen
            if (idx < len) {
                char[] newarr = new char[idx];
                System.arraycopy(arr, 0, newarr, 0, idx);
                Arrays.fill(arr, (char)0);
                arr = newarr;
            }

            return arr;
        }
        catch (IOException e) {
            Arrays.fill(arr, (char)0);
            throw e;
        }
    }

}
