/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.cryptolib.asymmetric.keypair.openssl;

import static java.util.Arrays.copyOfRange;
import static java.util.Arrays.fill;

import java.nio.charset.StandardCharsets;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;

/**
 * Encoded key specification which adapts PEM to PKCS#8. It also allows to
 * destroy the encoded key.
 */
class PEMEncodedKeySpec extends PKCS8EncodedKeySpec {
    private static final byte[] HEADER =
        "-----BEGIN PRIVATE KEY-----".getBytes(StandardCharsets.UTF_8);
    
    private static final byte[] FOOTER =
        "-----END PRIVATE KEY-----".getBytes(StandardCharsets.UTF_8);

    private static final byte[] DUMMY_ENCODED_KEY = {};

    private final byte[] encodedKey;

    private boolean destroyed;

    /**
     * Constructor. For internal use only.
     *
     * @param encodedKey
     */
    PEMEncodedKeySpec(final byte[] encodedKey) {
        super(DUMMY_ENCODED_KEY);
        this.encodedKey = encodedKey;
    }

    /**
     * Creates a new instance of {@link PEMEncodedKeySpec} for a given PEM.
     *
     * @param pem
     *            the PEM
     * @return the instance.
     * @throws CryptoLibException
     *             the PEM is malformed.
     */
    public static PEMEncodedKeySpec newInstance(final byte[] pem)
            throws CryptoLibException {
    	byte[] bytes = removeControlCharacters(pem);
        checkLength(bytes);
        checkStartsWithHeader(bytes);
        checkEndsWithFooter(bytes);
        return new PEMEncodedKeySpec(extractEncodedKey(bytes));
    }

	private static byte[] removeControlCharacters(final byte[] pem) {
		byte[] buffer = new byte[pem.length];
		int count = 0;
		for (byte b : pem) {
			if ((char)b >= ' ') {
				buffer[count++] = b;
			}
		}
		byte[] bytes = Arrays.copyOf(buffer, count);
		destroySensitive(pem);
		destroySensitive(buffer);
		return bytes;
	}

    private static void checkEndsWithFooter(final byte[] pem) {
        int offset = pem.length - FOOTER.length;
        for (int i = 0; i < FOOTER.length; i++) {
            if (FOOTER[i] != pem[offset + i]) {
                throw new CryptoLibException("PEM has invalid footer.");
            }
        }
    }

    private static void checkLength(final byte[] pem) {
        if (pem.length <= HEADER.length + FOOTER.length) {
        	throw new CryptoLibException("PEM has invalid length.");
        }
    }

    private static void checkStartsWithHeader(final byte[] pem) {
        for (int i = 0; i < HEADER.length; i++) {
            if (HEADER[i] != pem[i]) {
                throw new CryptoLibException("PEM has invalid header.");
            }
        }
    }

    private static void destroySensitive(final byte[] sensitive) {
        fill(sensitive, (byte) 0);
    }

    private static byte[] extractEncodedKey(final byte[] pem) {
        byte[] encodedKey;
        byte[] base64EncodedKey =
            copyOfRange(pem, HEADER.length , 
            		pem.length - FOOTER.length );
        try {
            encodedKey = Base64.getMimeDecoder().decode(base64EncodedKey);
        } finally {
            destroySensitive(base64EncodedKey);
        }
        return encodedKey;
    }

    /**
     * Destroys the keys specification.
     */
    public void destroy() {
        destroySensitive(encodedKey);
        destroyed = true;
    }

    /**
     * Returns the encoded key. Caller must not modify content of the returned
     * array, moreover the content will be erased when the specification is
     * destroyed with {@link #destroy()} method.
     *
     * @return the encoded key
     * @throws IllegalStateException
     *             the specification is destroyed.
     */
    @Override
    public byte[] getEncoded() {
        if (destroyed) {
            throw new IllegalStateException(
                "Key specification is destroyed");
        }
        return encodedKey;
    }
}
