/*
 * Copyright 2004 - 2013 Wayne Grant
 *           2013 - 2025 Kai Kramer
 *
 * This file is part of KeyStore Explorer.
 *
 * KeyStore Explorer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * KeyStore Explorer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with KeyStore Explorer.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kse.crypto.privatekey;

import static org.kse.crypto.privatekey.EncryptionType.ENCRYPTED;
import static org.kse.crypto.privatekey.EncryptionType.UNENCRYPTED;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.text.MessageFormat;
import java.util.ResourceBundle;
import java.util.Set;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import org.kse.crypto.CryptoException;
import org.kse.crypto.keypair.KeyPairType;
import org.kse.crypto.keypair.KeyPairUtil;
import org.kse.gui.passwordmanager.Password;
import org.kse.utilities.PRNG;
import org.kse.utilities.io.UnsignedUtil;

// @formatter:off
/**
 * Provides utility methods relating to Microsoft PVK encoded RSA and DSS (DSA)
 * private keys.
 *
 * <pre>
 * PVK format for RSA and DSS private keys:
 *
 *     DWORD magic     = PVK_MAGIC_NUMBER
 *     DWORD reserved  = PVK_RESERVED
 *     DWORD keytype   = PVK_KEY_EXCHANGE | PVK_KEY_SIGNATURE
 *     DWORD encrypted = PVK_UNENCRYPTED | PVK_ENCRYPTED
 *     DWORD saltlen   = UNENCRYPTED_SALT_LENGTH | ENCRYPTED_SALT_LENGTH
 *     DWORD keylen
 *     BYTE  salt[saltlen]
 *     BLOBHEADER blobheader
 *     PublicKeyBlob publicKeyBlob
 *
 *     BLOBHEADER:
 *
 *         BYTE bType      = PRIVATE_KEY_BLOB
 *         BYTE bVersion   = CUR_BLOB_VERSION
 *         WORD reserved   = BLOB_RESERVED
 *         ALG_ID aiKeyAlg = CALG_RSA_SIGN | CALG_RSA_KEYX | CALG_DSS_SIGN
 *
 *     RSA PublicKeyBlob:
 *
 *         RSAPUBKEY rsapubkey
 *         BYTE modulus[rsapubkey.bitlen/8]
 *         BYTE prime1[rsapubkey.bitlen/16]
 *         BYTE prime2[rsapubkey.bitlen/16]
 *         BYTE exponent1[rsapubkey.bitlen/16]
 *         BYTE exponent2[rsapubkey.bitlen/16]
 *         BYTE coefficient[rsapubkey.bitlen/16]
 *         BYTE privateExponent[rsapubkey.bitlen/8]
 *
 *         RSAPUBKEY:
 *
 *             DWORD magic = RSA_PRIV_MAGIC
 *             DWORD bitlen
 *             DWORD pubexp
 *
 *     DSS PublicKeyBlob:
 *
 *         DSSPUBKEY dsspubkey
 *         BYTE p[dsspubkey.bitlen/8]
 *         BYTE q[20]
 *         BYTE g[dsspubkey.bitlen/8]
 *         BYTE x[20]
 *         DSSSEED seedstruct
 *
 *         DSSPUBKEY:
 *
 *             DWORD magic = DSA_PRIV_MAGIC
 *             DWORD bitlen
 *
 *         DSSSEED:
 *
 *             DWORD counter
 *             BYTE seed[20]
 * </pre>
 *
 */
// @formatter:on
public class MsPvkUtil {
    private static ResourceBundle res = ResourceBundle.getBundle("org/kse/crypto/privatekey/resources");

    // Magic number field value
    private static final long PVK_MAGIC_NUMBER = 0xb0b5f11eL;

    // Reserved field value
    private static final int PVK_RESERVED = 0;

    // Key exchange value for key type
    public static final int PVK_KEY_EXCHANGE = 1;

    // Signature value for key type
    public static final int PVK_KEY_SIGNATURE = 2;

    // Unencrypted value for encrypted field
    private static final long PVK_UNENCRYPTED = 0;

    // Encrypted value for unencrypted field
    private static final int PVK_ENCRYPTED = 1;

    // Salt length for unencrypted PVK
    private static final long UNENCRYPTED_SALT_LENGTH = 0;

    // Salt length for encrypted PVK
    private static final int ENCRYPTED_SALT_LENGTH = 16;

    // PVK BLOB type field value
    private static final short PRIVATE_KEY_BLOB = 7;

    // PVK BLOB version field value
    private static final short CUR_BLOB_VERSION = 2;

    // PVK BLOB reserved field value
    private static final int BLOB_RESERVED = 0;

    // PVK key algorithm id field value for RSA signature private key
    private static final int CALG_RSA_SIGN = 0x2400;

    // PVK key algorithm id field value for RSA exchange private key
    private static final int CALG_RSA_KEYX = 0xa400;

    // PVK key algorithm id field value for DSA signature private key
    private static final int CALG_DSS_SIGN = 0x2200;

    // RSA private key magic number
    private static final int RSA_PRIV_MAGIC = 0x32415352;

    // DSS private key magic number
    private static final int DSS_PRIV_MAGIC = 0x32535344;

    // Length of a private key blob header in bytes
    private static final int BLOB_HEADER_LENGTH = 8;

    private static final Set<KeyPairType> PVK_SUPPORTED_KEYPAIR = Set.of(
            KeyPairType.RSA, KeyPairType.DSA);

    /*
     * Length of PVK read buffer used when creating PVK encoding This should be
     * large enough as even an encrypted 4096 RSA PVK only requires 2364 bytes
     */
    private static final int PVK_BUFFER_LENGTH = 8192;

    private MsPvkUtil() {
    }

    /**
     * Load an unencrypted PVK private key from the stream.
     *
     * @param pvk BA to load the unencrypted private key from
     * @return The private key
     * @throws PrivateKeyEncryptedException If private key is encrypted
     * @throws CryptoException              Problem encountered while loading the private key
     * @throws IOException                  An I/O error occurred
     */
    public static PrivateKey load(byte[] pvk) throws IOException, CryptoException {

        // Wrap in a byte buffer set up to read little endian
        ByteBuffer bb = ByteBuffer.wrap(pvk);
        bb.order(ByteOrder.LITTLE_ENDIAN);

        // Read and validate the reserved, magic number and key type fields - only returns the latter
        long keyType = readReservedMagicKeyType(bb);

        // Read and validate encrypted field
        long encrypted = UnsignedUtil.getInt(bb);

        if (encrypted != PVK_UNENCRYPTED) {
            throw new PrivateKeyEncryptedException(
                    MessageFormat.format(res.getString("MsPvkIsEncrypted.exception.message"),
                                         Long.toHexString(encrypted), Long.toHexString(PVK_UNENCRYPTED)));
        }

        // Read and validate salt length field
        long saltLength = UnsignedUtil.getInt(bb);

        if (saltLength != UNENCRYPTED_SALT_LENGTH) // Specific length (0) for unencrypted PVK
        {
            throw new CryptoException(
                    MessageFormat.format(res.getString("InvalidMsPvkSaltLengthField.exception.message"),
                                         Long.toHexString(saltLength), Long.toHexString(UNENCRYPTED_SALT_LENGTH)));
        }

        // Read key length
        long keyLength = UnsignedUtil.getInt(bb);

        // Read private key blob header
        readPrivateKeyBlobHeader(bb, keyType);

        // Read all remaining bytes as the private key blob
        byte[] privateKeyBlob = new byte[bb.remaining()];
        bb.get(privateKeyBlob);

        // Validate key length - should be length of key blob plus blob header
        if (keyLength != (privateKeyBlob.length + BLOB_HEADER_LENGTH)) {
            throw new CryptoException(
                    MessageFormat.format(res.getString("InvalidMsPvkKeyLengthField.exception.message"),
                                         Long.toHexString(keyLength),
                                         Long.toHexString(privateKeyBlob.length + BLOB_HEADER_LENGTH)));
        }

        return blobToPrivateKey(privateKeyBlob);
    }

    /**
     * Load an encrypted PVK private key from the specified stream.
     *
     * @param pvk      BA to load the encrypted private key from
     * @param password Password to decrypt
     * @return The private key
     * @throws PrivateKeyUnencryptedException If private key is unencrypted
     * @throws CryptoException                Problem encountered while loading the private key
     * @throws IOException                    An I/O error occurred
     */
    public static PrivateKey loadEncrypted(byte[] pvk, Password password) throws IOException, CryptoException {
        try {
            // Wrap in a byte buffer set up to read little endian
            ByteBuffer bb = ByteBuffer.wrap(pvk);
            bb.order(ByteOrder.LITTLE_ENDIAN);

            // Read and validate the reserved, magic number and key type fields - only returns the latter
            long keyType = readReservedMagicKeyType(bb);

            // Read and validate encrypted field
            long encrypted = UnsignedUtil.getInt(bb);

            if (encrypted != PVK_ENCRYPTED) {
                throw new PrivateKeyUnencryptedException(
                        MessageFormat.format(res.getString("MsPvkIsUnencrypted.exception.message"),
                                             Long.toHexString(encrypted), Long.toHexString(PVK_ENCRYPTED)));
            }

            // Read and validate salt length field
            long saltLength = UnsignedUtil.getInt(bb);

            if (saltLength != ENCRYPTED_SALT_LENGTH) { // Specific length for encrypted PVK
                throw new CryptoException(
                        MessageFormat.format(res.getString("InvalidMsPvkSaltLengthField.exception.message"),
                                             Long.toHexString(saltLength), Long.toHexString(ENCRYPTED_SALT_LENGTH)));
            }

            // Read key length
            long keyLength = UnsignedUtil.getInt(bb);

            // Create decryption keys
            byte[] strongKey = new byte[16]; // Strong version
            byte[] weakKey = new byte[16]; // Weak version

            // Read Salt
            byte[] salt = new byte[(int) saltLength];
            bb.get(salt);

            // Concatenate salt and password and derive key using SHA-1
            MessageDigest messagedigest = MessageDigest.getInstance("SHA1");

            byte[] passwordBytes = new String(password.toCharArray()).getBytes();
            byte[] saltAndPassword = new byte[salt.length + passwordBytes.length];

            System.arraycopy(salt, 0, saltAndPassword, 0, salt.length);
            System.arraycopy(passwordBytes, 0, saltAndPassword, salt.length, passwordBytes.length);

            byte[] key = messagedigest.digest(saltAndPassword);

            // Create strong key - first 16 bytes of key
            System.arraycopy(key, 0, strongKey, 0, 16);

            // Create weak key - first 5 bytes of key followed by 11 zero bytes
            System.arraycopy(key, 0, weakKey, 0, 5);

            for (int i = 5; i < 16; i++) {
                weakKey[i] = 0;
            }

            // Read private key blob header
            readPrivateKeyBlobHeader(bb, keyType);

            // Read all remaining bytes - the encrypted key
            byte[] encryptedPrivateKeyBlob = new byte[bb.remaining()];
            bb.get(encryptedPrivateKeyBlob);

            // Validate key length - should be length of encrypted key blob plus blob header
            if (keyLength != (encryptedPrivateKeyBlob.length + BLOB_HEADER_LENGTH)) {
                throw new CryptoException(
                        MessageFormat.format(res.getString("InvalidMsPvkKeyLengthField.exception.message"),
                                             Long.toHexString(keyLength),
                                             Long.toHexString(encryptedPrivateKeyBlob.length + BLOB_HEADER_LENGTH)));
            }

            // Decrypt key using RC4 with strong key
            byte[] decryptedPrivateKeyBlob = decryptPrivateKeyBlob(encryptedPrivateKeyBlob, strongKey);

            // Test for success
            if (decryptedPrivateKeyBlob == null) {
                // Failed - now try weak key
                decryptedPrivateKeyBlob = decryptPrivateKeyBlob(encryptedPrivateKeyBlob, weakKey);

                if (decryptedPrivateKeyBlob == null) {
                    // Failed - could not decrypt - password is most likely incorrect
                    throw new CryptoException(res.getString("NoDecryptMsPvkCheckPassword.exception.message"));
                }
            }

            return blobToPrivateKey(decryptedPrivateKeyBlob);
        } catch (GeneralSecurityException ex) {
            throw new CryptoException(res.getString("NoLoadMsPvk.exception.message"), ex);
        }
    }

    /**
     * PVK encode an RSA private key.
     *
     * @param privateKey The private key
     * @param keyType    PVK_KEY_EXCHANGE or PVK_KEY_SIGNATURE
     * @return The encoding
     * @throws CryptoException Problem encountered while getting the encoded private key
     */
    public static byte[] get(RSAPrivateCrtKey privateKey, int keyType) throws CryptoException {
        return getInternal(privateKey, keyType);
    }

    /**
     * PVK encode a DSA private key.
     *
     * @param privateKey The private key
     * @return The encoding
     * @throws CryptoException Problem encountered while getting the encoded private key
     */
    public static byte[] get(DSAPrivateKey privateKey) throws CryptoException {
        return getInternal(privateKey, PVK_KEY_SIGNATURE);
    }

    /**
     * Detect if a Microsoft PVK private key is encrypted or not.
     *
     * @param pvk BA containing Microsoft PVK private key
     * @return Encryption type or null if not a valid Microsoft PVK private key
     */
    public static EncryptionType getEncryptionType(byte[] pvk) {

        if (pvk == null || pvk.length < 24) {
            return null;
        }

        // Wrap in a byte buffer set up to read little endian
        ByteBuffer bb = ByteBuffer.wrap(pvk);
        bb.order(ByteOrder.LITTLE_ENDIAN);

        // Read and validate magic number field
        long magic = UnsignedUtil.getInt(bb);

        if (magic != PVK_MAGIC_NUMBER) {
            return null;
        }

        // Read and validate reserved field
        long reserved = UnsignedUtil.getInt(bb);

        if (reserved != PVK_RESERVED) {
            return null;
        }

        // Read and validate key type field
        long keyType = UnsignedUtil.getInt(bb);

        if ((keyType != PVK_KEY_EXCHANGE) && (keyType != PVK_KEY_SIGNATURE)) {
            return null;
        }

        // Read and validate encrypted field
        long encrypted = UnsignedUtil.getInt(bb);

        if (encrypted == PVK_ENCRYPTED) {
            return ENCRYPTED;
        } else if (encrypted == PVK_UNENCRYPTED) {
            return UNENCRYPTED;
        }

        return null;
    }

    private static byte[] getInternal(PrivateKey privateKey, int keyType) throws CryptoException {
        // PVK encode a private key unencrypted

        try {
            // Write PVK to a byte buffer set up to write little endian
            ByteBuffer bb = ByteBuffer.wrap(new byte[PVK_BUFFER_LENGTH]);
            bb.order(ByteOrder.LITTLE_ENDIAN);

            // Write magic number, reserved and key type fields
            writeReservedMagicKeyType(bb, keyType);

            // Get unencrypted private key blob
            byte[] privateKeyBlob;

            if (privateKey instanceof RSAPrivateCrtKey) {
                privateKeyBlob = rsaPrivateKeyToBlob((RSAPrivateCrtKey) privateKey);
            } else {
                privateKeyBlob = dsaPrivateKeyToBlob((DSAPrivateKey) privateKey);
            }

            // Write type field - unencrypted
            UnsignedUtil.putInt(bb, PVK_UNENCRYPTED);

            // Write salt length - unencrypted so no salt, length = 0
            UnsignedUtil.putInt(bb, UNENCRYPTED_SALT_LENGTH);

            // Write key length field - length of the blob plus length of blob header
            long keyLength = privateKeyBlob.length + BLOB_HEADER_LENGTH;
            UnsignedUtil.putInt(bb, keyLength);

            // Write private key blob header
            writePrivateKeyBlobHeader(bb, keyType, privateKey);

            // Write private key blob
            bb.put(privateKeyBlob);

            return getBufferBytes(bb);
        } catch (IOException ex) {
            throw new CryptoException(res.getString("NoGetMsPvk.exception.message"), ex);
        }
    }

    /**
     * PVK encode and encrypt an RSA private key.
     *
     * @param privateKey The RSA private key
     * @param keyType    PVK_KEY_EXCHANGE or PVK_KEY_SIGNATURE
     * @param password   Encryption password
     * @param strong     Use strong key for encryption?
     * @return The encoding
     * @throws CryptoException Problem encountered while getting the encoded private key
     */
    public static byte[] getEncrypted(RSAPrivateCrtKey privateKey, int keyType, Password password, boolean strong)
            throws CryptoException {
        return getEncryptedInternal(privateKey, keyType, password, strong);
    }

    /**
     * PVK encode and encrypt a DSA private key.
     *
     * @param privateKey The DSA private key
     * @param password   Encryption password
     * @param strong     Use strong key for encryption?
     * @return The encoding
     * @throws CryptoException Problem encountered while getting the encoded private key
     */
    public static byte[] getEncrypted(DSAPrivateKey privateKey, Password password, boolean strong)
            throws CryptoException {
        return getEncryptedInternal(privateKey, PVK_KEY_SIGNATURE, password, strong);
    }

    /**
     * Legacy format only supported for RSA and DSA keys
     */
    public static boolean isPVKFormatSupported(PrivateKey privateKey) {
        KeyPairType keyPairType = KeyPairUtil.getKeyPairType(privateKey);
        return PVK_SUPPORTED_KEYPAIR.contains(keyPairType);
    }

    private static byte[] getEncryptedInternal(PrivateKey privateKey, int keyType, Password password, boolean strong)
            throws CryptoException {
        // PVK encode a private key unencrypted

        try {
            // Write PVK to a byte buffer set up to write little endian
            ByteBuffer bb = ByteBuffer.wrap(new byte[PVK_BUFFER_LENGTH]);
            bb.order(ByteOrder.LITTLE_ENDIAN);

            // Write magic number, reserved and key type fields
            writeReservedMagicKeyType(bb, keyType);

            // Get password as bytes
            byte[] passwordBytes = new String(password.toCharArray()).getBytes();

            // Generate salt for encryption
            byte[] salt = generate16ByteSalt();

            // Concatenate the salt and password
            byte[] saltAndPassword = new byte[salt.length + passwordBytes.length];
            System.arraycopy(salt, 0, saltAndPassword, 0, salt.length);
            System.arraycopy(passwordBytes, 0, saltAndPassword, salt.length, passwordBytes.length);

            // Digest the salt and password to create the encryption key
            MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
            byte[] key = messageDigest.digest(saltAndPassword);

            // Get private key blob
            byte[] privateKeyBlob;

            if (privateKey instanceof RSAPrivateCrtKey) {
                privateKeyBlob = rsaPrivateKeyToBlob((RSAPrivateCrtKey) privateKey);
            } else {
                privateKeyBlob = dsaPrivateKeyToBlob((DSAPrivateKey) privateKey);
            }

            // Encrypt private key blob
            byte[] encryptedPrivateKeyBlob;

            if (strong) {
                // Strong version uses all 16 bytes of the key
                byte[] strongKey = new byte[16];
                System.arraycopy(key, 0, strongKey, 0, strongKey.length);
                encryptedPrivateKeyBlob = encryptPrivateKeyBlob(privateKeyBlob, strongKey);
            } else {
                // The weak version uses only 5 bytes of the key followed by 11 zero bytes
                byte[] weakKey = new byte[16];
                System.arraycopy(key, 0, weakKey, 0, 5);
                for (int i = 5; i < weakKey.length; i++) {
                    weakKey[i] = 0;
                }

                encryptedPrivateKeyBlob = encryptPrivateKeyBlob(privateKeyBlob, weakKey);
            }

            // Write type field - encrypted
            UnsignedUtil.putInt(bb, PVK_ENCRYPTED);

            // Write salt length field
            UnsignedUtil.putInt(bb, salt.length);

            // Write key length field - length of the blob plus length blob header
            int keyLength = encryptedPrivateKeyBlob.length + BLOB_HEADER_LENGTH;
            UnsignedUtil.putInt(bb, keyLength);

            // Write salt
            bb.put(salt);

            // Write private key blob header
            writePrivateKeyBlobHeader(bb, keyType, privateKey);

            // Write blob
            bb.put(encryptedPrivateKeyBlob);

            return getBufferBytes(bb);
        } catch (IOException | NoSuchAlgorithmException ex) {
            throw new CryptoException(res.getString("NoGetMsPvk.exception.message"), ex);
        }
    }

    private static void writeReservedMagicKeyType(ByteBuffer bb, long keyType) throws IOException, CryptoException {
        // Write the PVK fields: reserved, magic and key type

        UnsignedUtil.putInt(bb, PVK_MAGIC_NUMBER);

        UnsignedUtil.putInt(bb, PVK_RESERVED);

        if ((keyType != PVK_KEY_EXCHANGE) && (keyType != PVK_KEY_SIGNATURE)) {
            throw new CryptoException(MessageFormat.format(res.getString("InvalidMsPvkKeyTypeField.exception.message"),
                                                           Long.toHexString(keyType),
                                                           Long.toHexString(PVK_KEY_EXCHANGE),
                                                           Long.toHexString(PVK_KEY_SIGNATURE)));
        }

        UnsignedUtil.putInt(bb, keyType);
    }

    private static long readReservedMagicKeyType(ByteBuffer bb) throws CryptoException {
        /*
         * Read and validate the starting PVK fields: reserved, magic and key
         * type. While all three fields are validated only the key type is of
         * further interest and is therefore returned
         */

        long magic = UnsignedUtil.getInt(bb);

        if (magic != PVK_MAGIC_NUMBER) {
            throw new CryptoException(MessageFormat.format(res.getString("InvalidMsPvkMagicField.exception.message"),
                                                           Long.toHexString(magic),
                                                           Long.toHexString(PVK_MAGIC_NUMBER)));
        }

        long reserved = UnsignedUtil.getInt(bb);

        if (reserved != PVK_RESERVED) {
            throw new CryptoException(MessageFormat.format(res.getString("InvalidMsPvkReservedField.exception.message"),
                                                           Long.toHexString(reserved), Long.toHexString(PVK_RESERVED)));
        }

        long keyType = UnsignedUtil.getInt(bb);

        if ((keyType != PVK_KEY_EXCHANGE) && (keyType != PVK_KEY_SIGNATURE)) {
            throw new CryptoException(MessageFormat.format(res.getString("InvalidMsPvkKeyTypeField.exception.message"),
                                                           Long.toHexString(keyType),
                                                           Long.toHexString(PVK_KEY_EXCHANGE),
                                                           Long.toHexString(PVK_KEY_SIGNATURE)));
        }

        return keyType;
    }

    private static void writePrivateKeyBlobHeader(ByteBuffer bb, long keyType, PrivateKey privateKey)
            throws IOException {
        // Write Key blob type - private key
        UnsignedUtil.putByte(bb, PRIVATE_KEY_BLOB);

        // Write Blob version
        UnsignedUtil.putByte(bb, CUR_BLOB_VERSION);

        // Write Reserved value
        UnsignedUtil.putShort(bb, BLOB_RESERVED);

        // Write Algorithm ID - differs depending on key type and key pair type
        if (keyType == PVK_KEY_SIGNATURE) {
            if (privateKey instanceof RSAPrivateCrtKey) {
                UnsignedUtil.putInt(bb, CALG_RSA_SIGN); // RSA signature
            } else {
                UnsignedUtil.putInt(bb, CALG_DSS_SIGN); // DSA signature
            }
        } else {
            UnsignedUtil.putInt(bb, CALG_RSA_KEYX); // Key exchange - RSA only
        }
    }

    private static void readPrivateKeyBlobHeader(ByteBuffer bb, long keyType) throws CryptoException {
        // Read and validate BLOB header type field
        int blobType = UnsignedUtil.getByte(bb);

        if (blobType != PRIVATE_KEY_BLOB) {
            throw new CryptoException(
                    MessageFormat.format(res.getString("InvalidBlobHeaderTypeField.exception.message"),
                                         Integer.toHexString(blobType), Integer.toHexString(PRIVATE_KEY_BLOB)));
        }

        // Read and validate BLOB header version field
        short blobVersion = UnsignedUtil.getByte(bb);

        if (blobVersion != CUR_BLOB_VERSION) {
            throw new CryptoException(
                    MessageFormat.format(res.getString("InvalidBlobHeaderVersionField.exception.message"),
                                         Integer.toHexString(blobVersion), Integer.toHexString(CUR_BLOB_VERSION)));
        }

        // Read and validate BLOB header reserved field
        int blobReserved = UnsignedUtil.getShort(bb);

        if (blobReserved != BLOB_RESERVED) {
            throw new CryptoException(
                    MessageFormat.format(res.getString("InvalidBlobHeaderReservedField.exception.message"),
                                         Integer.toHexString(blobReserved), Integer.toHexString(BLOB_RESERVED)));
        }

        // Read and validate BLOB header algorithm ID

        long keyAlgId = UnsignedUtil.getInt(bb);

        // Only accept RSA or DSS - algorithm ID must match key type
        if (keyType == PVK_KEY_SIGNATURE) {
            // RSA or DSS for signature
            if ((keyAlgId != CALG_RSA_SIGN) && (keyAlgId != CALG_DSS_SIGN)) {
                throw new CryptoException(
                        MessageFormat.format(res.getString("InvalidBlobHeaderKeyAlgIdField.exception.message"),
                                             Long.toHexString(keyAlgId), Long.toHexString(CALG_RSA_SIGN),
                                             Long.toHexString(CALG_DSS_SIGN)));
            }
        } else {
            // RSA for key exchange - DSS cannot be used for key exchange
            if (keyAlgId != CALG_RSA_KEYX) {
                throw new CryptoException(
                        MessageFormat.format(res.getString("InvalidBlobHeaderKeyAlgIdField.exception.message"),
                                             Long.toHexString(keyAlgId), Long.toHexString(CALG_RSA_KEYX)));
            }
        }
    }

    private static PrivateKey blobToPrivateKey(byte[] privateKeyBlob) throws CryptoException {
        if ((privateKeyBlob[0] == 82) && // R
            (privateKeyBlob[1] == 83) && // S
            (privateKeyBlob[2] == 65) && // A
            (privateKeyBlob[3] == 50)) // 2
        {
            return blobToRsaPrivateKey(privateKeyBlob);
        } else {
            return blobToDsaPrivateKey(privateKeyBlob);
        }
    }

    private static RSAPrivateCrtKey blobToRsaPrivateKey(byte[] rsaPrivateKeyBlob) throws CryptoException {
        try {
            ByteBuffer bb = ByteBuffer.wrap(rsaPrivateKeyBlob);
            bb.order(ByteOrder.LITTLE_ENDIAN);

            // Get each blob field

            long magic = UnsignedUtil.getInt(bb); // rsapubkey.magic

            // Check magic field is valid
            if (magic != RSA_PRIV_MAGIC) {
                throw new CryptoException(MessageFormat.format(res.getString("InvalidRsaMagicField.exception.message"),
                                                               Long.toHexString(magic),
                                                               Long.toHexString(RSA_PRIV_MAGIC)));
            }

            long bitLength = UnsignedUtil.getInt(bb); // rsapubkey.bitlen

            // Byte lengths divisions may have remainders to take account for if not factors of 16 and/or 8
            int add8 = 0;
            if ((bitLength % 8) != 0) {
                add8++;
            }

            int add16 = 0;
            if ((bitLength % 16) != 0) {
                add16++;
            }

            BigInteger publicExponent = new BigInteger(Long.toString(UnsignedUtil.getInt(bb))); // rsapubkey.pubexp

            BigInteger modulus = readBigInteger(bb, (int) (bitLength / 8) + add8); // modulus
            BigInteger prime1 = readBigInteger(bb, (int) (bitLength / 16) + add16); // prime 1
            BigInteger prime2 = readBigInteger(bb, (int) (bitLength / 16) + add16); // prime 2
            BigInteger exponent1 = readBigInteger(bb, (int) (bitLength / 16) + add16); // exponent1
            BigInteger exponent2 = readBigInteger(bb, (int) (bitLength / 16) + add16); // exponent2
            BigInteger coefficient = readBigInteger(bb, (int) (bitLength / 16) + add16); // coefficient
            BigInteger privateExponent = readBigInteger(bb, (int) (bitLength / 8) + add8); // privateExponent

            RSAPrivateCrtKeySpec rsaPrivateCrtKeySpec = new RSAPrivateCrtKeySpec(modulus, publicExponent,
                                                                                 privateExponent, prime1, prime2,
                                                                                 exponent1, exponent2, coefficient);

            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return (RSAPrivateCrtKey) keyFactory.generatePrivate(rsaPrivateCrtKeySpec);
        } catch (IOException | GeneralSecurityException ex) {
            throw new CryptoException(res.getString("NoConvertBlobToRsaKey.exception.message"), ex);
        }
    }

    private static DSAPrivateKey blobToDsaPrivateKey(byte[] dsaPrivateKeyBlob) throws CryptoException {
        try {
            ByteBuffer bb = ByteBuffer.wrap(dsaPrivateKeyBlob);
            bb.order(ByteOrder.LITTLE_ENDIAN);

            // Get each blob field

            long magic = UnsignedUtil.getInt(bb); // dsspubkey.magic

            // Check magic field is valid
            if (magic != DSS_PRIV_MAGIC) {
                throw new CryptoException(MessageFormat.format(res.getString("InvalidDsaMagicField.exception.message"),
                                                               Long.toHexString(magic),
                                                               Long.toHexString(DSS_PRIV_MAGIC)));
            }

            long bitLength = UnsignedUtil.getInt(bb); // dsspubkey.bitlen

            BigInteger p = readBigInteger(bb, (int) (bitLength / 8)); // modulus
            BigInteger q = readBigInteger(bb, 20); // prime
            BigInteger g = readBigInteger(bb, (int) (bitLength / 8)); // generator
            BigInteger x = readBigInteger(bb, 20); // secret exponent

            // Ignore 24 bytes of dssseed (only applicable to public keys)
            for (int i = 0; i < 24; i++) {
                bb.get();
            }

            DSAPrivateKeySpec dsaPrivateKeySpec = new DSAPrivateKeySpec(x, p, q, g);

            KeyFactory keyFactory = KeyFactory.getInstance("DSA");
            return (DSAPrivateKey) keyFactory.generatePrivate(dsaPrivateKeySpec);
        } catch (IOException | GeneralSecurityException ex) {
            throw new CryptoException(res.getString("NoConvertBlobToDsaKey.exception.message"), ex);
        }
    }

    private static byte[] rsaPrivateKeyToBlob(RSAPrivateCrtKey rsaPrivCrtKey) throws CryptoException {
        try {
            ByteBuffer bb = ByteBuffer.wrap(new byte[4096]); // 2316 sufficient for a 4096 bit RSA key
            bb.order(ByteOrder.LITTLE_ENDIAN);

            // Write out the blob fields

            UnsignedUtil.putInt(bb, RSA_PRIV_MAGIC); // rsapubkey.magic

            BigInteger modulus = rsaPrivCrtKey.getModulus();
            int bitLength = modulus.bitLength();
            UnsignedUtil.putInt(bb, bitLength); // rsapubkey.bitlen

            BigInteger publicExponent = rsaPrivCrtKey.getPublicExponent();
            UnsignedUtil.putInt(bb, (int) publicExponent.longValue()); // rsapubkey.pubexp

            /*
             * Byte lengths divisions may have remainders to take account for if
             * not factors of 16 and/or 8
             */
            int add8 = 0;
            if ((bitLength % 8) != 0) {
                add8++;
            }

            int add16 = 0;
            if ((bitLength % 16) != 0) {
                add16++;
            }

            writeBigInteger(bb, modulus, (bitLength / 8) + add8); // modulus
            writeBigInteger(bb, rsaPrivCrtKey.getPrimeP(), (bitLength / 16) + add16); // prime1
            writeBigInteger(bb, rsaPrivCrtKey.getPrimeQ(), (bitLength / 16) + add16); // prime2
            writeBigInteger(bb, rsaPrivCrtKey.getPrimeExponentP(), (bitLength / 16) + add16); // exponent1
            writeBigInteger(bb, rsaPrivCrtKey.getPrimeExponentQ(), (bitLength / 16) + add16); // exponent2
            writeBigInteger(bb, rsaPrivCrtKey.getCrtCoefficient(), (bitLength / 16) + add16); // coefficient
            writeBigInteger(bb, rsaPrivCrtKey.getPrivateExponent(), (bitLength / 8) + add8); // privateExponent

            return getBufferBytes(bb);
        } catch (IOException ex) {
            throw new CryptoException(res.getString("NoConvertKeyToBlob.exception.message"), ex);
        }
    }

    private static byte[] dsaPrivateKeyToBlob(DSAPrivateKey dsaPrivKey) throws CryptoException {
        try {
            DSAParams dsaParams = dsaPrivKey.getParams();

            ByteBuffer bb = ByteBuffer.wrap(new byte[512]); // 328 sufficient for a 1024 bit DSA key
            bb.order(ByteOrder.LITTLE_ENDIAN);

            // Write out the blob fields

            UnsignedUtil.putInt(bb, DSS_PRIV_MAGIC); // dsspubkey.magic

            BigInteger prime = dsaParams.getP();
            int bitLength = prime.toString(2).length();
            UnsignedUtil.putInt(bb, bitLength); // dsspubkey.bitlen

            /*
             * Unlike RSA there are no bit length remainders (ie DSA bit length
             * always divisible by 8 as they are multiples of 64)
             */

            writeBigInteger(bb, dsaParams.getP(), (bitLength / 8)); // modulus
            writeBigInteger(bb, dsaParams.getQ(), 20); // prime
            writeBigInteger(bb, dsaParams.getG(), (bitLength / 8)); // generator
            writeBigInteger(bb, dsaPrivKey.getX(), 20); // secret exponent

            UnsignedUtil.putInt(bb, 0xffffffff); // dssseed.counter - none, fill 0xff

            for (int i = 0; i < 20; i++) // dssseed.seed - none, fill 0xff
            {
                bb.put((byte) 0xff);
            }

            return getBufferBytes(bb);
        } catch (IOException ex) {
            throw new CryptoException(res.getString("NoConvertKeyToBlob.exception.message"), ex);
        }
    }

    private static byte[] decryptPrivateKeyBlob(byte[] encryptedPvk, byte[] rc4Key) throws CryptoException {
        // Decrypt encrypted private key blob using RC4

        try {
            SecretKeySpec rc4KeySpec = new SecretKeySpec(rc4Key, "RC4");
            Cipher rc42 = Cipher.getInstance("RC4");
            rc42.init(Cipher.DECRYPT_MODE, rc4KeySpec);
            byte[] decryptedKeyBlob = rc42.doFinal(encryptedPvk);

            // Test if key decryption was successful

            // First four bytes will be "RSA2" if successful for an RSA private key
            if ((decryptedKeyBlob[0] == 82) && // R
                (decryptedKeyBlob[1] == 83) && // S
                (decryptedKeyBlob[2] == 65) && // A
                (decryptedKeyBlob[3] == 50)) // 2
            {
                return decryptedKeyBlob;
            }
            // First four bytes will be "DSS2" if successful for a DSA private key
            else if ((decryptedKeyBlob[0] == 68) && // D
                     (decryptedKeyBlob[1] == 83) && // S
                     (decryptedKeyBlob[2] == 83) && // S
                     (decryptedKeyBlob[3] == 50)) // 2
            {
                return decryptedKeyBlob;
            } else {
                return null;
            }
        } catch (GeneralSecurityException ex) {
            throw new CryptoException(res.getString("PrivateKeyBlobRc4DecryptionFailed.exception.message"), ex);
        }
    }

    private static byte[] encryptPrivateKeyBlob(byte[] privateKeyBlob, byte[] rc4Key) throws CryptoException {
        // Encrypt private key blob using RC4

        try {
            SecretKeySpec rc4KeySpec = new SecretKeySpec(rc4Key, "RC4");
            Cipher rc42 = Cipher.getInstance("RC4");
            rc42.init(Cipher.ENCRYPT_MODE, rc4KeySpec);

            return rc42.doFinal(privateKeyBlob);
        } catch (GeneralSecurityException ex) {
            throw new CryptoException(res.getString("PrivateKeyBlobRc4EncryptionFailed.exception.message"), ex);
        }
    }

    private static byte[] generate16ByteSalt() {
        return PRNG.generate(ENCRYPTED_SALT_LENGTH);
    }

    private static byte[] getBufferBytes(ByteBuffer bb) {
        byte[] buffer = bb.array();

        // Read all the byte's in the backing array to the extent writing
        // took place
        byte[] written = new byte[bb.position()];

        System.arraycopy(buffer, 0, written, 0, written.length);

        return written;
    }

    private static BigInteger readBigInteger(ByteBuffer bb, int length) throws IOException {
        // Read a big integer from a little endian source

        // Read the required number of bytes
        byte[] bigIntBytes = new byte[length];
        bb.get(bigIntBytes);

        /*
         * The byte buffer's byte order does not apply for get() so reverse to
         * convert from little to big endian
         */
        reverseBytes(bigIntBytes);

        // Construct and return the big integer

        return new BigInteger(1, bigIntBytes);
    }

    private static void writeBigInteger(ByteBuffer bb, BigInteger bigInteger, int length) throws IOException {
        // Get big-endian two's compliment representation of big integer
        byte[] bigInt = bigInteger.toByteArray();

        // Remove leading zero bytes
        int skipZeroPos = 0;

        for (byte b : bigInt) {
            if (b != 0) {
                break;
            }
            skipZeroPos++;
        }

        byte[] tmp = new byte[bigInt.length - skipZeroPos];

        System.arraycopy(bigInt, skipZeroPos, tmp, 0, tmp.length);

        bigInt = tmp;

        // Convert to little-endian
        reverseBytes(bigInt);

        // Pad out byte array with zeros
        int padByteLength = length - bigInt.length;

        if (padByteLength > 0) {
            tmp = new byte[length];
            System.arraycopy(bigInt, 0, tmp, 0, bigInt.length);
            bigInt = tmp;
        }

        bb.put(bigInt);
    }

    private static void reverseBytes(byte[] bytes) {
        int halfWay = bytes.length / 2;
        for (int i = 0; i < halfWay; i++) {
            byte b = bytes[i];
            bytes[i] = bytes[bytes.length - 1 - i];
            bytes[bytes.length - 1 - i] = b;
        }
    }
}
