/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * 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.
 */
/*
 * Copyright (c) 2015-2017, The Linux Foundation.
 */
/*
 * Contributed by: Giesecke & Devrient GmbH.
 */

package com.android.se;

import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.se.omapi.ISecureElementChannel;
import android.se.omapi.ISecureElementListener;
import android.se.omapi.SEService;
import android.util.Log;

import com.android.se.SecureElementService.SecureElementSession;
import com.android.se.security.ChannelAccess;

import java.io.IOException;

/**
 * Represents a Channel opened with the Secure Element
 */
public class Channel implements IBinder.DeathRecipient {

    private final String mTag = "SecureElement-Channel";
    private final int mChannelNumber;
    private final Object mLock = new Object();
    private IBinder mBinder = null;
    private boolean mIsClosed;
    private SecureElementSession mSession;
    private Terminal mTerminal;
    private byte[] mSelectResponse;
    private ChannelAccess mChannelAccess = null;
    private int mCallingPid = 0;
    private byte[] mAid = null;

    Channel(SecureElementSession session, Terminal terminal, int channelNumber,
            byte[] selectResponse, byte[] aid, ISecureElementListener listener) {
        if (terminal == null) {
            throw new IllegalArgumentException("Arguments can't be null");
        }
        mSession = session;
        mTerminal = terminal;
        mIsClosed = false;
        mSelectResponse = selectResponse;
        mChannelNumber = channelNumber;
        mAid = aid;
        if (listener != null) {
            try {
                mBinder = listener.asBinder();
                mBinder.linkToDeath(this, 0);
            } catch (RemoteException e) {
                Log.e(mTag, "Failed to register client listener");
            }
        }
    }

    /**
     * Close this channel if the client died.
     */
    public void binderDied() {
        try {
            Log.e(mTag, Thread.currentThread().getName() + " Client "
                    + mBinder.toString() + " died");
            close();
        } catch (Exception ignore) {
        }
    }

    /**
     * Closes the channel.
     */
    public void close() {
        synchronized (mLock) {
            if (isClosed())
                return;
            mIsClosed = true;
        }
        if (isBasicChannel()) {
            Log.i(mTag, "Close basic channel - Select without AID ...");
            mTerminal.selectDefaultApplication();
        }

        mTerminal.closeChannel(this);
        if (mBinder != null) {
            mBinder.unlinkToDeath(this, 0);
        }
        if (mSession != null) {
            mSession.removeChannel(this);
        }
    }

    /**
     * Transmits the given byte and returns the response.
     */
    public byte[] transmit(byte[] command) throws IOException {
        if (isClosed()) {
            throw new IllegalStateException("Channel is closed");
        }
        if (command == null) {
            throw new NullPointerException("Command must not be null");
        }
        if (mChannelAccess == null) {
            throw new SecurityException("Channel access not set");
        }
        if (mChannelAccess.getCallingPid() != mCallingPid) {
            throw new SecurityException("Wrong Caller PID.");
        }

        // Validate the APDU command format and throw IllegalArgumentException, if necessary.
        CommandApduValidator.execute(command);

        if (((command[0] & (byte) 0x80) == 0)
                && ((command[0] & (byte) 0x60) != (byte) 0x20)) {
            // ISO command
            if (command[1] == (byte) 0x70) {
                throw new SecurityException("MANAGE CHANNEL command not allowed");
            }
            if ((command[1] == (byte) 0xA4) && (command[2] == (byte) 0x04)) {
                // SELECT by DF name is only allowed for CarrierPrivilege applications
                // or system privilege applications
                if (ChannelAccess.ACCESS.ALLOWED != mChannelAccess.getPrivilegeAccess()) {
                    throw new SecurityException("SELECT by DF name command not allowed");
                }
            }
        }

        checkCommand(command);
        synchronized (mLock) {
            // set channel number bits
            command[0] = setChannelToClassByte(command[0], mChannelNumber);
            return mTerminal.transmit(command);
        }
    }

    private boolean selectNext() throws IOException {
        if (isClosed()) {
            throw new IllegalStateException("Channel is closed");
        } else if (mChannelAccess == null) {
            throw new IllegalStateException("Channel access not set.");
        } else if (mChannelAccess.getCallingPid() != mCallingPid) {
            throw new SecurityException("Wrong Caller PID.");
        } else if (mAid == null || mAid.length == 0) {
            throw new UnsupportedOperationException("No aid given");
        }

        byte[] selectCommand = new byte[5 + mAid.length];
        selectCommand[0] = 0x00;
        selectCommand[1] = (byte) 0xA4;
        selectCommand[2] = 0x04;
        selectCommand[3] = 0x02; // next occurrence
        selectCommand[4] = (byte) mAid.length;
        System.arraycopy(mAid, 0, selectCommand, 5, mAid.length);

        // set channel number bits
        selectCommand[0] = setChannelToClassByte(selectCommand[0], mChannelNumber);

        byte[] bufferSelectResponse = mTerminal.transmit(selectCommand);

        if (bufferSelectResponse.length < 2) {
            throw new UnsupportedOperationException("Transmit failed");
        }
        int sw1 = bufferSelectResponse[bufferSelectResponse.length - 2] & 0xFF;
        int sw2 = bufferSelectResponse[bufferSelectResponse.length - 1] & 0xFF;
        int sw = (sw1 << 8) | sw2;

        if (((sw & 0xF000) == 0x9000) || ((sw & 0xFF00) == 0x6200)
                || ((sw & 0xFF00) == 0x6300)) {
            mSelectResponse = bufferSelectResponse.clone();
            return true;
        } else if ((sw & 0xFF00) == 0x6A00) {
            return false;
        } else {
            throw new UnsupportedOperationException("Unsupported operation");
        }
    }

    /**
     * Returns a copy of the given CLA byte where the channel number bits are set
     * as specified by the given channel number
     *
     * <p>See GlobalPlatform Card Specification 2.2.0.7: 11.1.4 Class Byte Coding
     *
     * @param cla           the CLA byte. Won't be modified
     * @param channelNumber within [0..3] (for first inter-industry class byte
     *                      coding) or [4..19] (for further inter-industry class byte coding)
     * @return the CLA byte with set channel number bits. The seventh bit
     * indicating the used coding
     * (first/further interindustry class byte coding) might be modified
     */
    private byte setChannelToClassByte(byte cla, int channelNumber) {
        if (channelNumber < 4) {
            // b7 = 0 indicates the first interindustry class byte coding
            cla = (byte) ((cla & 0xBC) | channelNumber);
        } else if (channelNumber < 20) {
            // b7 = 1 indicates the further interindustry class byte coding
            boolean isSm = (((cla & 0x40) == 0x00) && ((cla & 0x0C) != 0));
            cla = (byte) ((cla & 0xB0) | 0x40 | (channelNumber - 4));
            if (isSm) {
                cla |= 0x20;
            }
        } else {
            throw new IllegalArgumentException("Channel number must be within [0..19]");
        }
        return cla;
    }

    public ChannelAccess getChannelAccess() {
        return this.mChannelAccess;
    }

    public void setChannelAccess(ChannelAccess channelAccess) {
        this.mChannelAccess = channelAccess;
    }

    private void setCallingPid(int pid) {
        mCallingPid = pid;
    }

    private void checkCommand(byte[] command) {
        if (mTerminal.getAccessControlEnforcer() != null) {
            // check command if it complies to the access rules.
            // if not an exception is thrown
            mTerminal.getAccessControlEnforcer().checkCommand(this, command);
        } else {
            // Allow access to Privileged App even if Access Control Enforcer is
            // not initialized
            if (ChannelAccess.ACCESS.ALLOWED != mChannelAccess.getPrivilegeAccess()) {
                throw new SecurityException("Access Controller not set for Terminal: "
                        + mTerminal.getName());
            }
        }
    }

    /**
     * true if aid could be selected during opening the channel
     * false if aid could not be or was not selected.
     *
     * @return boolean.
     */
    public boolean hasSelectedAid() {
        return (mAid != null);
    }

    public int getChannelNumber() {
        return mChannelNumber;
    }

    /**
     * Returns the data as received from the application select command
     * inclusively the status word.
     *
     * The returned byte array contains the data bytes in the following order:
     * first data byte, ... , last data byte, sw1, sw2
     *
     * @return null if an application SELECT command has not been performed or
     * the selection response can not be retrieved by the reader
     * implementation.
     */
    public byte[] getSelectResponse() {
        return (hasSelectedAid() ? mSelectResponse : null);
    }

    public boolean isBasicChannel() {
        return (mChannelNumber == 0) ? true : false;
    }

    public boolean isClosed() {
        return mIsClosed;
    }

    // Implementation of the SecureElement Channel interface according to OMAPI.
    final class SecureElementChannel extends ISecureElementChannel.Stub {

        @Override
        public void close() throws RemoteException {
            Channel.this.close();
        }

        @Override
        public boolean isClosed() throws RemoteException {
            return Channel.this.isClosed();
        }

        @Override
        public boolean isBasicChannel() throws RemoteException {
            return Channel.this.isBasicChannel();
        }

        @Override
        public byte[] getSelectResponse() throws RemoteException {
            return Channel.this.getSelectResponse();
        }

        @Override
        public byte[] transmit(byte[] command) throws RemoteException {
            Channel.this.setCallingPid(Binder.getCallingPid());
            try {
                return Channel.this.transmit(command);
            } catch (IOException e) {
                throw new ServiceSpecificException(SEService.IO_ERROR, e.getMessage());
            }
        }

        @Override
        public boolean selectNext() throws RemoteException {
            Channel.this.setCallingPid(Binder.getCallingPid());
            try {
                return Channel.this.selectNext();
            } catch (IOException e) {
                throw new ServiceSpecificException(SEService.IO_ERROR, e.getMessage());
            }
        }

        @Override
        public String getInterfaceHash() {
            return ISecureElementChannel.HASH;
        }

        @Override
        public int getInterfaceVersion() {
            return ISecureElementChannel.VERSION;
        }
    }
}
