/*
 * Copyright (C) 2017 Schürmann & Breitmoser GbR
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.sufficientlysecure.keychain.remote;


import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;

import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.openintents.ssh.authentication.ISshAuthenticationService;
import org.openintents.ssh.authentication.SshAuthenticationApi;
import org.openintents.ssh.authentication.SshAuthenticationApiError;
import org.openintents.ssh.authentication.response.KeySelectionResponse;
import org.openintents.ssh.authentication.response.PublicKeyResponse;
import org.openintents.ssh.authentication.response.SigningResponse;
import org.openintents.ssh.authentication.response.SshPublicKeyResponse;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.daos.ApiAppDao;
import org.sufficientlysecure.keychain.daos.KeyRepository;
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
import org.sufficientlysecure.keychain.model.SubKey.UnifiedKeyInfo;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey;
import org.sufficientlysecure.keychain.pgp.SshPublicKey;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ssh.AuthenticationData;
import org.sufficientlysecure.keychain.ssh.AuthenticationOperation;
import org.sufficientlysecure.keychain.ssh.AuthenticationParcel;
import org.sufficientlysecure.keychain.ssh.AuthenticationResult;
import org.sufficientlysecure.keychain.ssh.signature.SshSignatureConverter;
import timber.log.Timber;


public class SshAuthenticationService extends Service {
    private ApiPermissionHelper mApiPermissionHelper;
    private KeyRepository mKeyRepository;
    private ApiAppDao mApiAppDao;
    private ApiPendingIntentFactory mApiPendingIntentFactory;

    private static final List<Integer> SUPPORTED_VERSIONS = Collections.unmodifiableList(Collections.singletonList(1));
    private static final int INVALID_API_VERSION = -1;

    private static final int HASHALGORITHM_NONE = SshAuthenticationApiError.INVALID_HASH_ALGORITHM;

    @Override
    public void onCreate() {
        super.onCreate();
        mApiPermissionHelper = new ApiPermissionHelper(this, ApiAppDao.getInstance(this));
        mKeyRepository = KeyRepository.create(this);
        mApiAppDao = ApiAppDao.getInstance(this);

        mApiPendingIntentFactory = new ApiPendingIntentFactory(getBaseContext());
    }

    private final ISshAuthenticationService.Stub mSSHAgent = new ISshAuthenticationService.Stub() {
        @Override
        public Intent execute(Intent intent) {
            return checkIntent(intent);
        }

    };

    @Override
    public IBinder onBind(Intent intent) {
        return mSSHAgent;
    }

    private Intent checkIntent(Intent intent) {
        Intent errorResult = checkRequirements(intent);
        if (errorResult == null) {
            return executeInternal(intent);
        } else {
            return errorResult;
        }
    }

    private Intent executeInternal(Intent intent) {
        switch (intent.getAction()) {
            case SshAuthenticationApi.ACTION_SIGN:
                return authenticate(intent);
            case SshAuthenticationApi.ACTION_SELECT_KEY:
                return getAuthenticationKey(intent);
            case SshAuthenticationApi.ACTION_GET_PUBLIC_KEY:
                return getAuthenticationPublicKey(intent, false);
            case SshAuthenticationApi.ACTION_GET_SSH_PUBLIC_KEY:
                return getAuthenticationPublicKey(intent, true);
            default:
                return createErrorResult(SshAuthenticationApiError.UNKNOWN_ACTION, "Unknown action");
        }
    }

    private Intent authenticate(Intent data) {
        Intent errorIntent = checkForKeyId(data);
        if (errorIntent != null) {
            return errorIntent;
        }

        // keyid == masterkeyid -> authkeyid
        // keyId is the pgp master keyId, the keyId used will be the first authentication
        // key in the keyring designated by the master keyId
        String keyIdString = data.getStringExtra(SshAuthenticationApi.EXTRA_KEY_ID);
        long masterKeyId = Long.valueOf(keyIdString);

        int hashAlgorithmTag = getHashAlgorithm(data);
        if (hashAlgorithmTag == HASHALGORITHM_NONE) {
            return createErrorResult(SshAuthenticationApiError.GENERIC_ERROR, "No valid hash algorithm!");
        }

        byte[] challenge = data.getByteArrayExtra(SshAuthenticationApi.EXTRA_CHALLENGE);
        if (challenge == null || challenge.length == 0) {
            return createErrorResult(SshAuthenticationApiError.GENERIC_ERROR, "No challenge given");
        }

        // carries the metadata necessary for authentication
        AuthenticationData.Builder authData = AuthenticationData.builder();
        authData.setAuthenticationMasterKeyId(masterKeyId);

        long authSubKeyId;
        int authSubKeyAlgorithm;
        String authSubKeyCurveOid = null;
        try {
            // get first usable subkey capable of authentication
            authSubKeyId = mKeyRepository.getEffectiveAuthenticationKeyId(masterKeyId);
            // needed for encoding the resulting signature
            authSubKeyAlgorithm = getPublicKey(masterKeyId).getAlgorithm();
            if (authSubKeyAlgorithm == PublicKeyAlgorithmTags.ECDSA) {
                authSubKeyCurveOid = getPublicKey(masterKeyId).getCurveOid();
            }
        } catch (NotFoundException e) {
            return createExceptionErrorResult(SshAuthenticationApiError.NO_SUCH_KEY,
                    "Key for master key id not found", e);
        }

        authData.setAuthenticationSubKeyId(authSubKeyId);

        authData.setAllowedAuthenticationKeyIds(getAllowedKeyIds());

        authData.setHashAlgorithm(hashAlgorithmTag);

        CryptoInputParcel inputParcel = CryptoInputParcelCacheService.getCryptoInputParcel(this, data);
        if (inputParcel == null) {
            // fresh request, assign UUID
            inputParcel = CryptoInputParcel.createCryptoInputParcel(new Date());
        }

        AuthenticationParcel authParcel = AuthenticationParcel
                .createAuthenticationParcel(authData.build(), challenge);

        // execute authentication operation!
        AuthenticationOperation authOperation = new AuthenticationOperation(this, mKeyRepository);
        AuthenticationResult authResult = authOperation.execute(authData.build(), inputParcel, authParcel);

        if (authResult.isPending()) {
            RequiredInputParcel requiredInput = authResult.getRequiredInputParcel();
            PendingIntent pi = mApiPendingIntentFactory.requiredInputPi(data, requiredInput,
                    authResult.mCryptoInputParcel);
            // return PendingIntent to be executed by client
            return packagePendingIntent(pi);
        } else if (authResult.success()) {
            byte[] rawSignature = authResult.getSignature();
            byte[] sshSignature;
            try {
                switch (authSubKeyAlgorithm) {
                    case PublicKeyAlgorithmTags.EDDSA:
                        sshSignature = SshSignatureConverter.getSshSignatureEdDsa(rawSignature);
                        break;
                    case PublicKeyAlgorithmTags.RSA_SIGN:
                    case PublicKeyAlgorithmTags.RSA_GENERAL:
                        sshSignature = SshSignatureConverter.getSshSignatureRsa(rawSignature, hashAlgorithmTag);
                        break;
                    case PublicKeyAlgorithmTags.ECDSA:
                        sshSignature = SshSignatureConverter.getSshSignatureEcDsa(rawSignature, authSubKeyCurveOid);
                        break;
                    case PublicKeyAlgorithmTags.DSA:
                        sshSignature = SshSignatureConverter.getSshSignatureDsa(rawSignature);
                        break;
                    default:
                        throw new NoSuchAlgorithmException("Unknown algorithm");
                }
            } catch (NoSuchAlgorithmException e) {
                return createExceptionErrorResult(SshAuthenticationApiError.INTERNAL_ERROR,
                        "Error converting signature", e);
            }
            return new SigningResponse(sshSignature).toIntent();
        } else {
            LogEntryParcel errorMsg = authResult.getLog().getLast();
            return createErrorResult(SshAuthenticationApiError.INTERNAL_ERROR, getString(errorMsg.mType.getMsgId()));
        }
    }

    private Intent checkForKeyId(Intent data) {
        long authMasterKeyId = getKeyId(data);
        if (authMasterKeyId == Constants.key.none) {
            return createErrorResult(SshAuthenticationApiError.NO_KEY_ID,
                    "No key id in request");
        }
        return null;
    }

    private long getKeyId(Intent data) {
        String keyIdString = data.getStringExtra(SshAuthenticationApi.EXTRA_KEY_ID);
        long authMasterKeyId = Constants.key.none;
        if (keyIdString != null) {
            try {
                authMasterKeyId = Long.valueOf(keyIdString);
            } catch (NumberFormatException e) {
                return Constants.key.none;
            }
        }
        return authMasterKeyId;
    }

    private int getHashAlgorithm(Intent data) {
        int hashAlgorithm = data.getIntExtra(SshAuthenticationApi.EXTRA_HASH_ALGORITHM, HASHALGORITHM_NONE);

        switch (hashAlgorithm) {
            case SshAuthenticationApi.SHA1:
                return HashAlgorithmTags.SHA1;
            case SshAuthenticationApi.RIPEMD160:
                return HashAlgorithmTags.RIPEMD160;
            case SshAuthenticationApi.SHA224:
                return HashAlgorithmTags.SHA224;
            case SshAuthenticationApi.SHA256:
                return HashAlgorithmTags.SHA256;
            case SshAuthenticationApi.SHA384:
                return HashAlgorithmTags.SHA384;
            case SshAuthenticationApi.SHA512:
                return HashAlgorithmTags.SHA512;
            default:
                return HASHALGORITHM_NONE;
        }
    }

    private Intent getAuthenticationKey(Intent data) {
        long masterKeyId = getKeyId(data);
        if (masterKeyId != Constants.key.none) {
            String description;

            try {
                description = getDescription(masterKeyId);
            } catch (NotFoundException e) {
                return createExceptionErrorResult(SshAuthenticationApiError.NO_SUCH_KEY,
                        "Could not create description", e);
            }

            return new KeySelectionResponse(String.valueOf(masterKeyId), description).toIntent();
        } else {
            return redirectToKeySelection(data);
        }
    }

    private Intent redirectToKeySelection(Intent data) {
        String currentPkg = mApiPermissionHelper.getCurrentCallingPackage();
        PendingIntent pi = mApiPendingIntentFactory.createSelectAuthenticationKeyIdPendingIntent(data, currentPkg);
        return packagePendingIntent(pi);
    }

    private Intent packagePendingIntent(PendingIntent pi) {
        Intent result = new Intent();
        result.putExtra(SshAuthenticationApi.EXTRA_RESULT_CODE,
                SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED);
        result.putExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT, pi);
        return result;
    }

    private Intent getAuthenticationPublicKey(Intent data, boolean asSshKey) {
        long masterKeyId = getKeyId(data);
        if (masterKeyId != Constants.key.none) {
            try {
                if (asSshKey) {
                    return getSSHPublicKey(masterKeyId);
                } else {
                    return getX509PublicKey(masterKeyId);
                }
            } catch (KeyRepository.NotFoundException e) {
                return createExceptionErrorResult(SshAuthenticationApiError.NO_SUCH_KEY,
                        "Key for master key id not found", e);
            } catch (PgpKeyNotFoundException e) {
                return createExceptionErrorResult(SshAuthenticationApiError.NO_AUTH_KEY,
                        "Authentication key for master key id not found in keychain", e);
            } catch (NoSuchAlgorithmException e) {
                return createExceptionErrorResult(SshAuthenticationApiError.INVALID_ALGORITHM,
                        "Algorithm not supported", e);
            }
        } else {
            return createErrorResult(SshAuthenticationApiError.NO_KEY_ID,
                    "No key id in request");
        }
    }

    private Intent getX509PublicKey(long masterKeyId) throws KeyRepository.NotFoundException, PgpKeyNotFoundException, NoSuchAlgorithmException {
        byte[] encodedPublicKey;
        int algorithm;

        PublicKey publicKey;
        try {
            publicKey = getPublicKey(masterKeyId).getJcaPublicKey();
        } catch (PgpGeneralException e) { // this should probably never happen
            return createExceptionErrorResult(SshAuthenticationApiError.GENERIC_ERROR,
                    "Error converting public key", e);
        }

        encodedPublicKey = publicKey.getEncoded();
        algorithm = translateAlgorithm(publicKey.getAlgorithm());

        return new PublicKeyResponse(encodedPublicKey, algorithm).toIntent();
    }

    private int translateAlgorithm(String algorithm) throws NoSuchAlgorithmException {
        switch (algorithm) {
            case "RSA":
                return SshAuthenticationApi.RSA;
            case "ECDSA":
                return SshAuthenticationApi.ECDSA;
            case "EdDSA":
                return SshAuthenticationApi.EDDSA;
            case "DSA":
                return SshAuthenticationApi.DSA;
            default:
                throw new NoSuchAlgorithmException("Error matching key algorithm to API supported algorithm: "
                        + algorithm);
        }
    }

    private Intent getSSHPublicKey(long masterKeyId) throws KeyRepository.NotFoundException {
        CanonicalizedPublicKey publicKey = getPublicKey(masterKeyId);

        SshPublicKey sshPublicKey = new SshPublicKey(publicKey);
        String sshPublicKeyBlob;
        try {
            sshPublicKeyBlob = sshPublicKey.getEncodedKey();
        } catch (PgpGeneralException | NoSuchAlgorithmException e) {
            return createExceptionErrorResult(SshAuthenticationApiError.GENERIC_ERROR,
                    "Error converting public key to SSH format", e);
        }

        return new SshPublicKeyResponse(sshPublicKeyBlob).toIntent();
    }

    private CanonicalizedPublicKey getPublicKey(long masterKeyId) throws NotFoundException {
        KeyRepository keyRepository = KeyRepository.create(getApplicationContext());
        long authKeyId = keyRepository.getEffectiveAuthenticationKeyId(masterKeyId);
        return keyRepository.getCanonicalizedPublicKeyRing(masterKeyId).getPublicKey(authKeyId);
    }

    private String getDescription(long masterKeyId) throws NotFoundException {
        UnifiedKeyInfo unifiedKeyInfo = mKeyRepository.getUnifiedKeyInfo(masterKeyId);

        String description = "";
        long authSubKeyId = mKeyRepository.getEffectiveAuthenticationKeyId(masterKeyId);
        description += unifiedKeyInfo.user_id();
        description += " (" + Long.toHexString(authSubKeyId) + ")";

        return description;
    }

    private HashSet<Long> getAllowedKeyIds() {
        String currentPkg = mApiPermissionHelper.getCurrentCallingPackage();
        return mApiAppDao.getAllowedKeyIdsForApp(currentPkg);
    }

    /**
     * @return null if basic requirements are met
     */
    private Intent checkRequirements(Intent data) {
        if (data == null) {
            return createErrorResult(SshAuthenticationApiError.GENERIC_ERROR, "No parameter bundle");
        }

        // check version
        int apiVersion = data.getIntExtra(SshAuthenticationApi.EXTRA_API_VERSION, INVALID_API_VERSION);
        if (!SUPPORTED_VERSIONS.contains(apiVersion)) {
            String errorMsg = "Incompatible API versions:\n"
                    + "used : " + data.getIntExtra(SshAuthenticationApi.EXTRA_API_VERSION, INVALID_API_VERSION) + "\n"
                    + "supported : " + SUPPORTED_VERSIONS;

            return createErrorResult(SshAuthenticationApiError.INCOMPATIBLE_API_VERSIONS, errorMsg);
        }

        // check if caller is allowed to access OpenKeychain
        Intent result = mApiPermissionHelper.isAllowedOrReturnIntent(data);
        if (result != null) {
            return result; // disallowed, redirect to registration
        }

        return null;
    }

    private Intent createErrorResult(int errorCode, String errorMessage) {
        Timber.e(errorMessage);
        Intent result = new Intent();
        result.putExtra(SshAuthenticationApi.EXTRA_ERROR, new SshAuthenticationApiError(errorCode, errorMessage));
        result.putExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, SshAuthenticationApi.RESULT_CODE_ERROR);
        return result;
    }

    private Intent createExceptionErrorResult(int errorCode, String errorMessage, Exception e) {
        String message = errorMessage + " : " + e.getMessage();
        return createErrorResult(errorCode, message);
    }

}
