/*
 *******************************************************************************
 *   FIDO U2F Authenticator
 *   (c) 2015 Ledger
 *
 *  Licensed 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.ledger.u2f;

import javacard.framework.JCSystem;
import javacard.security.RandomData;
import javacard.framework.Util;
import javacard.security.*;
import javacardx.crypto.Cipher;

public class FIDOStandalone implements FIDOAPI {

    private KeyPair keyPair;
    private AESKey chipKey;
    private Cipher cipherEncrypt;
    private Cipher cipherDecrypt;
    private RandomData random;
    private byte[] scratch;

    private static final byte[] IV_ZERO_AES = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

    /**
     * Init cipher engines and allocate memory.
     */
    public FIDOStandalone() {
        scratch = JCSystem.makeTransientByteArray((short) 64, JCSystem.CLEAR_ON_DESELECT);
        keyPair = new KeyPair(
        (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, KeyBuilder.LENGTH_EC_FP_256, false),
        (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, KeyBuilder.LENGTH_EC_FP_256, false));
        Secp256r1.setCommonCurveParameters((ECKey) keyPair.getPrivate());
        Secp256r1.setCommonCurveParameters((ECKey) keyPair.getPublic());
        random = RandomData.getInstance(RandomData.ALG_KEYGENERATION);
        // Initialize the unique wrapping key
        chipKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false);
        random.nextBytes(scratch, (short) 0, (short) 32);
        chipKey.setKey(scratch, (short) 0);
        cipherEncrypt = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD, false);
        cipherEncrypt.init(chipKey, Cipher.MODE_ENCRYPT, IV_ZERO_AES, (short) 0, (short) IV_ZERO_AES.length);
        cipherDecrypt = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD, false);
        cipherDecrypt.init(chipKey, Cipher.MODE_DECRYPT, IV_ZERO_AES, (short) 0, (short) IV_ZERO_AES.length);
    }

    /**
     * Interleave two byte arrays into the target one, nibble by nibble.
     * Example:
     *  array1 =  [0x12, 0x34]
     *  array2 =  [0xab, 0xcd]
     *         -> [0x1a, 0x2b, 0x3c, 0x4d]
     * <p>
     * This is used to interleave the generated private key and the application parameter into two AES-CBC blocks,
     * as not doing so would result in the application parameter being encrypted as a block with an all zero IV which
     * would always result in the same first block for all generated private keys with the same application parameter
     * wrapped under the same wrapping key, which would break privacy of U2F.
     *
     * @param array1
     * @param array1Offset
     * @param array2
     * @param array2Offset
     * @param target
     * @param targetOffset
     * @param length 
     */
    private static void interleave(byte[] array1, short array1Offset, byte[] array2, short array2Offset, byte[] target, short targetOffset, short length) {
        for (short i = 0; i < length; i++) {
            short a = (short) (array1[(short) (array1Offset + i)] & 0xff);
            short b = (short) (array2[(short) (array2Offset + i)] & 0xff);
            target[(short) (targetOffset + 2 * i)] = (byte) ((short) (a & 0xf0) | (short) (b >> 4));
            target[(short) (targetOffset + 2 * i + 1)] = (byte) ((short) ((a & 0x0f) << 4) | (short) (b & 0x0f));
        }
    }

    /**
     * Deinterleave a byte array back into two arrays of half size.
     *  Example:
     *  src = [0x1a, 0x2b, 0x3c, 0x4d]
     *     -> [0x12, 0x34] and [0xab, 0xcd]
     *
     * @param src
     * @param srcOffset
     * @param array1
     * @param array1Offset
     * @param array2
     * @param array2Offset
     * @param length 
     */
    private static void deinterleave(byte[] src, short srcOffset, byte[] array1, short array1Offset, byte[] array2, short array2Offset, short length) {
        for (short i = 0; i < length; i++) {
            short a = (short) (src[(short) (srcOffset + 2 * i)] & 0xff);
            short b = (short) (src[(short) (srcOffset + 2 * i + 1)] & 0xff);
            array1[(short) (array1Offset + i)] = (byte) ((short) (a & 0xf0) | (short) (b >> 4));
            array2[(short) (array2Offset + i)] = (byte) (((short) (a & 0x0f) << 4) | (short) (b & 0x0f));
        }
    }

    /* @override */
    public short generateKeyAndWrap(byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey generatedPrivateKey, byte[] publicKey, short publicKeyOffset, byte[] keyHandle, short keyHandleOffset) {
        // Generate a new pair
        keyPair.genKeyPair();
        // Copy public key
        ((ECPublicKey) keyPair.getPublic()).getW(publicKey, publicKeyOffset);
        // Wrap keypair and application parameters
        ((ECPrivateKey) keyPair.getPrivate()).getS(scratch, (short) 0);
        interleave(applicationParameter, applicationParameterOffset, scratch, (short) 0, keyHandle, keyHandleOffset, (short) 32);
        cipherEncrypt.doFinal(keyHandle, keyHandleOffset, (short) 64, keyHandle, keyHandleOffset);
        Util.arrayFillNonAtomic(scratch, (short) 0, (short) 32, (byte) 0x00);
        return (short) 64;
    }

    /* @override */
    public boolean unwrap(byte[] keyHandle, short keyHandleOffset, short keyHandleLength, byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey unwrappedPrivateKey) {
        // Verify
        cipherDecrypt.doFinal(keyHandle, keyHandleOffset, (short) 64, keyHandle, keyHandleOffset);
        deinterleave(keyHandle, keyHandleOffset, scratch, (short) 0, scratch, (short) 32, (short) 32);
        if (!FIDOUtils.compareConstantTime(applicationParameter, applicationParameterOffset, scratch, (short) 0, (short) 32)) {
            Util.arrayFillNonAtomic(scratch, (short) 32, (short) 32, (byte) 0x00);
            Util.arrayFillNonAtomic(keyHandle, keyHandleOffset, (short) 64, (byte) 0x00);
            return false;
        }
        Util.arrayFillNonAtomic(keyHandle, keyHandleOffset, (short) 64, (byte) 0x00);
        if (unwrappedPrivateKey != null) {
            unwrappedPrivateKey.setS(scratch, (short) 32, (short) 32);
        }
        Util.arrayFillNonAtomic(scratch, (short) 32, (short) 32, (byte) 0x00);
        return true;
    }

}