/*
 * 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.pgp;


import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.security.spec.ECGenParameterSpec;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicBoolean;

import androidx.annotation.Nullable;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.nist.NISTNamedCurves;
import org.bouncycastle.bcpg.ECDHPublicBCPGKey;
import org.bouncycastle.bcpg.ECDSAPublicBCPGKey;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.bcpg.S2K;
import org.bouncycastle.bcpg.sig.Features;
import org.bouncycastle.bcpg.sig.KeyFlags;
import org.bouncycastle.bcpg.sig.RevocationReasonTags;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator;
import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.jce.spec.ElGamalParameterSpec;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyFlags;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector;
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.PGPDigestCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
import org.bouncycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Builder;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenKeyToCardOperationsBuilder;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenSignOperationsBuilder;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.Primes;
import org.sufficientlysecure.keychain.util.ProgressScaler;
import timber.log.Timber;


/**
 * This class is the single place where ALL operations that actually modify a PGP public or secret
 * key take place.
 * <p/>
 * Note that no android specific stuff should be done here, ie no imports from com.android.
 * <p/>
 * All operations support progress reporting to a Progressable passed on initialization.
 * This indicator may be null.
 */
public class PgpKeyOperation {

    private Stack<Progressable> mProgress;
    private AtomicBoolean mCancelled;

    public PgpKeyOperation(Progressable progress) {
        super();
        if (progress != null) {
            mProgress = new Stack<>();
            mProgress.push(progress);
        }
    }

    public PgpKeyOperation(Progressable progress, AtomicBoolean cancelled) {
        this(progress);
        mCancelled = cancelled;
    }

    private boolean checkCancelled() {
        return mCancelled != null && mCancelled.get();
    }

    private void subProgressPush(int from, int to) {
        if (mProgress == null) {
            return;
        }
        mProgress.push(new ProgressScaler(mProgress.peek(), from, to, 100));
    }
    private void subProgressPop() {
        if (mProgress == null) {
            return;
        }
        if (mProgress.size() == 1) {
            throw new RuntimeException("Tried to pop progressable without prior push! "
                    + "This is a programming error, please file a bug report.");
        }
        mProgress.pop();
    }

    private void progress(int message, int current) {
        if (mProgress == null) {
            return;
        }
        mProgress.peek().setProgress(message, current, 100);
    }

    private ECGenParameterSpec getEccParameterSpec(Curve curve) {
        switch (curve) {
            case NIST_P256: return new ECGenParameterSpec("P-256");
            case NIST_P384: return new ECGenParameterSpec("P-384");
            case NIST_P521: return new ECGenParameterSpec("P-521");

            // @see SaveKeyringParcel
            // case BRAINPOOL_P256: return new ECGenParameterSpec("brainpoolp256r1");
            // case BRAINPOOL_P384: return new ECGenParameterSpec("brainpoolp384r1");
            // case BRAINPOOL_P512: return new ECGenParameterSpec("brainpoolp512r1");
        }
        throw new RuntimeException("Invalid choice! (can't happen)");
    }

    /** Creates new secret key. */
    private PGPKeyPair createKey(SubkeyAdd add, Date creationTime, OperationLog log, int indent) {

        try {
            // Some safety checks
            if (add.getAlgorithm() == Algorithm.ECDH || add.getAlgorithm() == Algorithm.ECDSA) {
                if (add.getCurve() == null) {
                    log.add(LogType.MSG_CR_ERROR_NO_CURVE, indent);
                    return null;
                }
            } else if (add.getAlgorithm() != Algorithm.EDDSA) {
                if (add.getKeySize() == null) {
                    log.add(LogType.MSG_CR_ERROR_NO_KEYSIZE, indent);
                    return null;
                }
                if (add.getKeySize() < 2048) {
                    log.add(LogType.MSG_CR_ERROR_KEYSIZE_2048, indent);
                    return null;
                }
            }

            int algorithm;
            KeyPairGenerator keyGen;

            switch (add.getAlgorithm()) {
                case DSA: {
                    if ((add.getFlags() & (PGPKeyFlags.CAN_ENCRYPT_COMMS | PGPKeyFlags.CAN_ENCRYPT_STORAGE)) > 0) {
                        log.add(LogType.MSG_CR_ERROR_FLAGS_DSA, indent);
                        return null;
                    }
                    progress(R.string.progress_generating_dsa, 30);
                    keyGen = KeyPairGenerator.getInstance("DSA", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
                    keyGen.initialize(add.getKeySize(), new SecureRandom());
                    algorithm = PGPPublicKey.DSA;
                    break;
                }

                case ELGAMAL: {
                    if ((add.getFlags() & (PGPKeyFlags.CAN_SIGN | PGPKeyFlags.CAN_CERTIFY)) > 0) {
                        log.add(LogType.MSG_CR_ERROR_FLAGS_ELGAMAL, indent);
                        return null;
                    }
                    progress(R.string.progress_generating_elgamal, 30);
                    keyGen = KeyPairGenerator.getInstance("ElGamal", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
                    BigInteger p = Primes.getBestPrime(add.getKeySize());
                    BigInteger g = new BigInteger("2");

                    ElGamalParameterSpec elParams = new ElGamalParameterSpec(p, g);

                    keyGen.initialize(elParams);
                    algorithm = PGPPublicKey.ELGAMAL_ENCRYPT;
                    break;
                }

                case RSA: {
                    progress(R.string.progress_generating_rsa, 30);
                    keyGen = KeyPairGenerator.getInstance("RSA", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
                    keyGen.initialize(add.getKeySize(), new SecureRandom());

                    algorithm = PGPPublicKey.RSA_GENERAL;
                    break;
                }

                case ECDSA: {
                    if ((add.getFlags() & (PGPKeyFlags.CAN_ENCRYPT_COMMS | PGPKeyFlags.CAN_ENCRYPT_STORAGE)) > 0) {
                        log.add(LogType.MSG_CR_ERROR_FLAGS_ECDSA, indent);
                        return null;
                    }
                    progress(R.string.progress_generating_ecdsa, 30);
                    ECGenParameterSpec ecParamSpec = getEccParameterSpec(add.getCurve());
                    keyGen = KeyPairGenerator.getInstance("ECDSA", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
                    keyGen.initialize(ecParamSpec, new SecureRandom());

                    algorithm = PGPPublicKey.ECDSA;
                    break;
                }

                case EDDSA: {
                    if ((add.getFlags() & (PGPKeyFlags.CAN_ENCRYPT_COMMS | PGPKeyFlags.CAN_ENCRYPT_STORAGE)) > 0) {
                        log.add(LogType.MSG_CR_ERROR_FLAGS_ECDSA, indent);
                        return null;
                    }
                    progress(R.string.progress_generating_eddsa, 30);
                    keyGen = KeyPairGenerator.getInstance("ED25519", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
                    keyGen.initialize(256, new SecureRandom());

                    algorithm = PGPPublicKey.EDDSA;
                    break;
                }

                case ECDH: {
                    // make sure there are no sign or certify flags set
                    if ((add.getFlags() & (PGPKeyFlags.CAN_SIGN | PGPKeyFlags.CAN_CERTIFY)) > 0) {
                        log.add(LogType.MSG_CR_ERROR_FLAGS_ECDH, indent);
                        return null;
                    }
                    progress(R.string.progress_generating_ecdh, 30);
                    ECGenParameterSpec ecParamSpec = getEccParameterSpec(add.getCurve());
                    keyGen = KeyPairGenerator.getInstance("ECDH", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
                    keyGen.initialize(ecParamSpec, new SecureRandom());

                    algorithm = PGPPublicKey.ECDH;
                    break;
                }

                default: {
                    log.add(LogType.MSG_CR_ERROR_UNKNOWN_ALGO, indent);
                    return null;
                }
            }

            // build new key pair
            KeyPair keyPair = keyGen.generateKeyPair();
            return new JcaPGPKeyPair(algorithm, keyPair, creationTime);

        } catch(NoSuchProviderException | InvalidAlgorithmParameterException e) {
            throw new RuntimeException(e);
        } catch(NoSuchAlgorithmException e) {
            log.add(LogType.MSG_CR_ERROR_UNKNOWN_ALGO, indent);
            return null;
        } catch(PGPException e) {
            Timber.e(e, "internal pgp error");
            log.add(LogType.MSG_CR_ERROR_INTERNAL_PGP, indent);
            return null;
        }
    }

    public PgpEditKeyResult createSecretKeyRing(SaveKeyringParcel saveParcel) {

        OperationLog log = new OperationLog();
        int indent = 0;

        try {

            log.add(LogType.MSG_CR, indent);
            progress(R.string.progress_building_key, 0);
            indent += 1;

            if (saveParcel.getAddSubKeys().isEmpty()) {
                log.add(LogType.MSG_CR_ERROR_NO_MASTER, indent);
                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
            }

            if (saveParcel.getAddUserIds().isEmpty()) {
                log.add(LogType.MSG_CR_ERROR_NO_USER_ID, indent);
                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
            }

            SubkeyAdd certificationKey = saveParcel.getAddSubKeys().get(0);
            if ((certificationKey.getFlags() & KeyFlags.CERTIFY_OTHER) != KeyFlags.CERTIFY_OTHER) {
                log.add(LogType.MSG_CR_ERROR_NO_CERTIFY, indent);
                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
            }

            if (certificationKey.getExpiry() == null) {
                log.add(LogType.MSG_CR_ERROR_NULL_EXPIRY, indent);
                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
            }

            Date creationTime = new Date();

            subProgressPush(10, 30);
            PGPKeyPair keyPair = createKey(certificationKey, creationTime, log, indent);
            subProgressPop();

            // return null if this failed (an error will already have been logged by createKey)
            if (keyPair == null) {
                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
            }

            progress(R.string.progress_building_master_key, 40);

            PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder()
                    .build().get(PgpSecurityConstants.SECRET_KEY_SIGNATURE_CHECKSUM_HASH_ALGO);
            PGPSecretKey masterSecretKey = new PGPSecretKey(keyPair.getPrivateKey(), keyPair.getPublicKey(),
                    sha1Calc, true, null);

            PGPSecretKeyRing sKR = new PGPSecretKeyRing(
                    masterSecretKey.getEncoded(), new JcaKeyFingerprintCalculator());

            // Remove certification key from remaining SaveKeyringParcel
            Builder builder = SaveKeyringParcel.buildUpon(saveParcel);
            builder.getMutableAddSubKeys().remove(certificationKey);
            saveParcel = builder.build();

            subProgressPush(50, 100);
            CryptoInputParcel cryptoInput = CryptoInputParcel.createCryptoInputParcel(creationTime, new Passphrase(""));
            return internal(sKR, masterSecretKey, certificationKey.getFlags(), certificationKey.getExpiry(), cryptoInput, saveParcel, log, indent);

        } catch (PGPException e) {
            log.add(LogType.MSG_CR_ERROR_INTERNAL_PGP, indent);
            Timber.e(e, "pgp error encoding key");
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        } catch (IOException e) {
            Timber.e(e, "io error encoding key");
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

    }

    /** This method introduces a list of modifications specified by a SaveKeyringParcel to a
     * WrappedSecretKeyRing.
     *
     * This method relies on WrappedSecretKeyRing's canonicalization property!
     *
     * Note that PGPPublicKeyRings can not be directly modified. Instead, the corresponding
     * PGPSecretKeyRing must be modified and consequently consolidated with its public counterpart.
     * This is a natural workflow since pgp keyrings are immutable data structures: Old semantics
     * are changed by adding new certificates, which implicitly override older certificates.
     *
     * Note that this method does not care about any "special" type of master key. If unlocking
     * with a passphrase fails, the operation will fail with an unlocking error. More specific
     * handling of errors should be done in UI code!
     *
     * If the passphrase is null, only a restricted subset of operations will be available,
     * namely stripping of subkeys and changing the protection mode of dummy keys.
     *
     */
    public PgpEditKeyResult modifySecretKeyRing(CanonicalizedSecretKeyRing wsKR,
                                                CryptoInputParcel cryptoInput,
                                                SaveKeyringParcel saveParcel) {

        OperationLog log = new OperationLog();
        int indent = 0;

        /*
         * 1. Unlock private key
         * 2a. Add certificates for new user ids
         * 2b. Add revocations for revoked user ids
         * 3. If primary user id changed, generate new certificates for both old and new
         * 4a. For each subkey change, generate new subkey binding certificate
         * 4b. For each subkey revocation, generate new subkey revocation certificate
         * 5. Generate and add new subkeys
         * 6. If requested, change passphrase
         */

        log.add(LogType.MSG_MF, indent,
                KeyFormattingUtils.convertKeyIdToHex(wsKR.getMasterKeyId()));
        indent += 1;
        progress(R.string.progress_building_key, 0);

        // Make sure this is called with a proper SaveKeyringParcel
        if (saveParcel.getMasterKeyId() == null || saveParcel.getMasterKeyId() != wsKR.getMasterKeyId()) {
            log.add(LogType.MSG_MF_ERROR_KEYID, indent);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        // We work on bouncycastle object level here
        PGPSecretKeyRing sKR = wsKR.getRing();
        PGPSecretKey masterSecretKey = sKR.getSecretKey();

        // Make sure the fingerprint matches
        if (saveParcel.getFingerprint() == null || !Arrays.equals(saveParcel.getFingerprint(),
                                    masterSecretKey.getPublicKey().getFingerprint())) {
            log.add(LogType.MSG_MF_ERROR_FINGERPRINT, indent);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        if (isParcelEmpty(saveParcel)) {
            log.add(LogType.MSG_MF_ERROR_NOOP, indent);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        saveParcel = parseSecurityTokenSerialNumberIntoSubkeyChanges(cryptoInput, saveParcel);

        if (!checkCapabilitiesAreUnique(wsKR, saveParcel, log, indent)) {
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        if (isDummy(masterSecretKey) && ! isParcelRestrictedOnly(saveParcel)) {
            log.add(LogType.MSG_EK_ERROR_DUMMY, indent);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        if (isDummy(masterSecretKey) || isParcelRestrictedOnly(saveParcel)) {
            log.add(LogType.MSG_MF_RESTRICTED_MODE, indent);
            return internalRestricted(sKR, saveParcel, log, indent + 1);
        }

        // Do we require a passphrase? If so, pass it along
        if (!isDivertToCard(masterSecretKey) && !cryptoInput.hasPassphraseForSubkey(masterSecretKey.getKeyID())) {
            log.add(LogType.MSG_MF_REQUIRE_PASSPHRASE, indent);
            return new PgpEditKeyResult(log, RequiredInputParcel.createRequiredSignPassphrase(
                    masterSecretKey.getKeyID(), masterSecretKey.getKeyID(),
                    cryptoInput.getSignatureTime()), cryptoInput);
        }

        // read masterKeyFlags, and use the same as before.
        // since this is the master key, this contains at least CERTIFY_OTHER
        PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();
        int masterKeyFlags = readKeyFlags(masterPublicKey) | KeyFlags.CERTIFY_OTHER;
        Date expiryTime = wsKR.getPublicKey().getExpiryTime();
        long masterKeyExpiry = expiryTime != null ? expiryTime.getTime() / 1000 : 0L;

        return internal(sKR, masterSecretKey, masterKeyFlags, masterKeyExpiry, cryptoInput, saveParcel, log, indent);

    }

    private SaveKeyringParcel parseSecurityTokenSerialNumberIntoSubkeyChanges(CryptoInputParcel cryptoInput,
            SaveKeyringParcel saveParcel) {
        SaveKeyringParcel.Builder builder = SaveKeyringParcel.buildUpon(saveParcel);
        for (SubkeyChange change : saveParcel.getChangeSubKeys()) {
            if (change.getMoveKeyToSecurityToken()) {
                // If this is a moveKeyToSecurityToken operation, see if it was completed: look for a hash
                // matching the given subkey ID in cryptoData.
                byte[] subKeyId = new byte[8];
                ByteBuffer buf = ByteBuffer.wrap(subKeyId);
                buf.putLong(change.getSubKeyId()).rewind();

                byte[] serialNumber = cryptoInput.getCryptoData().get(buf);
                if (serialNumber != null) {
                    builder.addOrReplaceSubkeyChange(
                            SubkeyChange.createSecurityTokenSerialNo(change.getSubKeyId(), serialNumber));
                }
            }
        }
        saveParcel = builder.build();
        return saveParcel;
    }

    private boolean checkCapabilitiesAreUnique(CanonicalizedSecretKeyRing wsKR, SaveKeyringParcel saveParcel,
            OperationLog log, int indent) {
        boolean hasSign = false;
        boolean hasEncrypt = false;
        boolean hasAuth = false;

        for (SubkeyChange change : saveParcel.getChangeSubKeys()) {
            if (change.getMoveKeyToSecurityToken()) {
                // Pending moveKeyToSecurityToken operation. Need to make sure that we don't have multiple
                // subkeys pending for the same slot.
                CanonicalizedSecretKey wsK = wsKR.getSecretKey(change.getSubKeyId());

                if ((wsK.canSign() || wsK.canCertify())) {
                    if (hasSign) {
                        log.add(LogType.MSG_MF_ERROR_DUPLICATE_KEYTOCARD_FOR_SLOT, indent + 1);
                        return false;
                    } else {
                        hasSign = true;
                    }
                } else if ((wsK.canEncrypt())) {
                    if (hasEncrypt) {
                        log.add(LogType.MSG_MF_ERROR_DUPLICATE_KEYTOCARD_FOR_SLOT, indent + 1);
                        return false;
                    } else {
                        hasEncrypt = true;
                    }
                } else if ((wsK.canAuthenticate())) {
                    if (hasAuth) {
                        log.add(LogType.MSG_MF_ERROR_DUPLICATE_KEYTOCARD_FOR_SLOT, indent + 1);
                        return false;
                    } else {
                        hasAuth = true;
                    }
                } else {
                    log.add(LogType.MSG_MF_ERROR_INVALID_FLAGS_FOR_KEYTOCARD, indent + 1);
                    return false;
                }
            }
        }
        return true;
    }

    private PgpEditKeyResult internal(PGPSecretKeyRing sKR, PGPSecretKey masterSecretKey,
                                     int masterKeyFlags, long masterKeyExpiry,
                                     CryptoInputParcel cryptoInput,
                                     SaveKeyringParcel saveParcel,
                                     OperationLog log,
                                     int indent) {

        SecurityTokenSignOperationsBuilder nfcSignOps = new SecurityTokenSignOperationsBuilder(
                cryptoInput.getSignatureTime(), masterSecretKey.getKeyID(),
                masterSecretKey.getKeyID());
        SecurityTokenKeyToCardOperationsBuilder nfcKeyToCardOps = new SecurityTokenKeyToCardOperationsBuilder(
                masterSecretKey.getKeyID());

        progress(R.string.progress_modify, 0);

        PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();

        PGPPrivateKey masterPrivateKey;

        if (isDivertToCard(masterSecretKey)) {
            masterPrivateKey = null;
            log.add(LogType.MSG_MF_DIVERT, indent);
        } else {

            // 1. Unlock private key
            progress(R.string.progress_modify_unlock, 10);
            log.add(LogType.MSG_MF_UNLOCK, indent);
            {
                try {
                    PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
                            Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(cryptoInput.getPassphrase().getCharArray());
                    masterPrivateKey = masterSecretKey.extractPrivateKey(keyDecryptor);
                } catch (PGPException e) {
                    log.add(LogType.MSG_MF_UNLOCK_ERROR, indent + 1);
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                }
            }
        }

        try {

            // Check if we were cancelled
            if (checkCancelled()) {
                log.add(LogType.MSG_OPERATION_CANCELLED, indent);
                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_CANCELLED, log, null);
            }

            { // work on master secret key

                PGPPublicKey modifiedPublicKey = masterPublicKey;

                // 2a. Add certificates for new user ids
                subProgressPush(15, 23);
                String changePrimaryUserId = saveParcel.getChangePrimaryUserId();
                for (int i = 0; i < saveParcel.getAddUserIds().size(); i++) {

                    progress(R.string.progress_modify_adduid, (i - 1) * (100 / saveParcel.getAddUserIds().size()));
                    String userId = saveParcel.getAddUserIds().get(i);
                    log.add(LogType.MSG_MF_UID_ADD, indent, userId);

                    if ("".equals(userId)) {
                        log.add(LogType.MSG_MF_UID_ERROR_EMPTY, indent + 1);
                        return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                    }

                    // this operation supersedes all previous binding and revocation certificates,
                    // so remove those to retain assertions from canonicalization for later operations
                    @SuppressWarnings("unchecked")
                    Iterator<PGPSignature> it = modifiedPublicKey.getSignaturesForID(userId);
                    if (it != null) {
                        for (PGPSignature cert : new IterableIterator<>(it)) {
                            if (cert.getKeyID() != masterPublicKey.getKeyID()) {
                                // foreign certificate?! error error error
                                log.add(LogType.MSG_MF_ERROR_INTEGRITY, indent);
                                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                            }
                            if (cert.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION
                                    || cert.getSignatureType() == PGPSignature.NO_CERTIFICATION
                                    || cert.getSignatureType() == PGPSignature.CASUAL_CERTIFICATION
                                    || cert.getSignatureType() == PGPSignature.POSITIVE_CERTIFICATION
                                    || cert.getSignatureType() == PGPSignature.DEFAULT_CERTIFICATION) {
                                modifiedPublicKey = PGPPublicKey.removeCertification(
                                        modifiedPublicKey, userId, cert);
                            }
                        }
                    }

                    // if it's supposed to be primary, we can do that here as well
                    boolean isPrimary = changePrimaryUserId != null
                            && userId.equals(changePrimaryUserId);
                    // generate and add new certificate
                    try {
                        PGPSignature cert = generateUserIdSignature(
                                getSignatureGenerator(masterSecretKey, cryptoInput),
                                cryptoInput.getSignatureTime(),
                                masterPrivateKey, masterPublicKey, userId,
                                isPrimary, masterKeyFlags, masterKeyExpiry);
                        modifiedPublicKey = PGPPublicKey.addCertification(modifiedPublicKey, userId, cert);
                    } catch (NfcInteractionNeeded e) {
                        nfcSignOps.addHash(e.hashToSign, e.hashAlgo);
                    }
                }
                subProgressPop();

                // 2b. Add certificates for new user ids
                subProgressPush(23, 32);
                List<WrappedUserAttribute> addUserAttributes = saveParcel.getAddUserAttribute();
                for (int i = 0; i < addUserAttributes.size(); i++) {
                    progress(R.string.progress_modify_adduat, (i - 1) * (100 / addUserAttributes.size()));
                    WrappedUserAttribute attribute = addUserAttributes.get(i);

                    switch (attribute.getType()) {
                        // the 'none' type must not succeed
                        case WrappedUserAttribute.UAT_NONE:
                            log.add(LogType.MSG_MF_UAT_ERROR_EMPTY, indent);
                            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                        case WrappedUserAttribute.UAT_IMAGE:
                            log.add(LogType.MSG_MF_UAT_ADD_IMAGE, indent);
                            break;
                        default:
                            log.add(LogType.MSG_MF_UAT_ADD_UNKNOWN, indent);
                            break;
                    }

                    PGPUserAttributeSubpacketVector vector = attribute.getVector();

                    // generate and add new certificate
                    try {
                        PGPSignature cert = generateUserAttributeSignature(
                                getSignatureGenerator(masterSecretKey, cryptoInput),
                                cryptoInput.getSignatureTime(),
                                masterPrivateKey, masterPublicKey, vector,
                                masterKeyFlags, masterKeyExpiry);
                        modifiedPublicKey = PGPPublicKey.addCertification(modifiedPublicKey, vector, cert);
                    } catch (NfcInteractionNeeded e) {
                        nfcSignOps.addHash(e.hashToSign, e.hashAlgo);
                    }
                }
                subProgressPop();

                // 2c. Add revocations for revoked user ids
                subProgressPush(32, 40);
                List<String> revokeUserIds = saveParcel.getRevokeUserIds();
                for (int i = 0, j = revokeUserIds.size(); i < j; i++) {
                    progress(R.string.progress_modify_revokeuid, (i - 1) * (100 / revokeUserIds.size()));
                    String userId = revokeUserIds.get(i);
                    log.add(LogType.MSG_MF_UID_REVOKE, indent, userId);

                    // Make sure the user id exists (yes these are 10 LoC in Java!)
                    boolean exists = false;
                    //noinspection unchecked
                    for (String uid : new IterableIterator<String>(modifiedPublicKey.getUserIDs())) {
                        if (userId.equals(uid)) {
                            exists = true;
                            break;
                        }
                    }
                    if (!exists) {
                        log.add(LogType.MSG_MF_ERROR_NOEXIST_REVOKE, indent);
                        return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                    }

                    // a duplicate revocation will be removed during canonicalization, so no need to
                    // take care of that here.
                    try {
                        PGPSignature cert = generateRevocationSignature(
                                getSignatureGenerator(masterSecretKey, cryptoInput),
                                cryptoInput.getSignatureTime(),
                                masterPrivateKey, masterPublicKey, userId);
                        modifiedPublicKey = PGPPublicKey.addCertification(modifiedPublicKey, userId, cert);
                    } catch (NfcInteractionNeeded e) {
                        nfcSignOps.addHash(e.hashToSign, e.hashAlgo);
                    }
                }
                subProgressPop();

                // 3. If primary user id changed, generate new certificates for both old and new
                if (changePrimaryUserId != null) {
                    progress(R.string.progress_modify_primaryuid, 40);

                    // keep track if we actually changed one
                    boolean ok = false;
                    log.add(LogType.MSG_MF_UID_PRIMARY, indent, changePrimaryUserId);
                    indent += 1;

                    // we work on the modifiedPublicKey here, to respect new or newly revoked uids
                    // noinspection unchecked
                    for (String userId : new IterableIterator<String>(modifiedPublicKey.getUserIDs())) {
                        boolean isRevoked = false;
                        PGPSignature currentCert = null;
                        // noinspection unchecked
                        for (PGPSignature cert : new IterableIterator<PGPSignature>(
                                modifiedPublicKey.getSignaturesForID(userId))) {
                            if (cert.getKeyID() != masterPublicKey.getKeyID()) {
                                // foreign certificate?! error error error
                                log.add(LogType.MSG_MF_ERROR_INTEGRITY, indent);
                                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                            }
                            // we know from canonicalization that if there is any revocation here, it
                            // is valid and not superseded by a newer certification.
                            if (cert.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION) {
                                isRevoked = true;
                                continue;
                            }
                            // we know from canonicalization that there is only one binding
                            // certification here, so we can just work with the first one.
                            if (cert.getSignatureType() == PGPSignature.NO_CERTIFICATION ||
                                    cert.getSignatureType() == PGPSignature.CASUAL_CERTIFICATION ||
                                    cert.getSignatureType() == PGPSignature.POSITIVE_CERTIFICATION ||
                                    cert.getSignatureType() == PGPSignature.DEFAULT_CERTIFICATION) {
                                currentCert = cert;
                            }
                        }

                        if (currentCert == null) {
                            // no certificate found?! error error error
                            log.add(LogType.MSG_MF_ERROR_INTEGRITY, indent);
                            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                        }

                        // we definitely should not update certifications of revoked keys, so just leave it.
                        if (isRevoked) {
                            // revoked user ids cannot be primary!
                            if (userId.equals(changePrimaryUserId)) {
                                log.add(LogType.MSG_MF_ERROR_REVOKED_PRIMARY, indent);
                                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                            }
                            continue;
                        }

                        // if this is~ the/a primary user id
                        if (currentCert.getHashedSubPackets() != null
                                && currentCert.getHashedSubPackets().isPrimaryUserID()) {
                            // if it's the one we want, just leave it as is
                            if (userId.equals(changePrimaryUserId)) {
                                ok = true;
                                continue;
                            }
                            // otherwise, generate new non-primary certification
                            log.add(LogType.MSG_MF_PRIMARY_REPLACE_OLD, indent);
                            modifiedPublicKey = PGPPublicKey.removeCertification(
                                    modifiedPublicKey, userId, currentCert);
                            try {
                                PGPSignature newCert = generateUserIdSignature(
                                        getSignatureGenerator(masterSecretKey, cryptoInput),
                                        cryptoInput.getSignatureTime(),
                                        masterPrivateKey, masterPublicKey, userId, false,
                                        masterKeyFlags, masterKeyExpiry);
                                modifiedPublicKey = PGPPublicKey.addCertification(
                                        modifiedPublicKey, userId, newCert);
                            } catch (NfcInteractionNeeded e) {
                                nfcSignOps.addHash(e.hashToSign, e.hashAlgo);
                            }

                            continue;
                        }

                        // if we are here, this is not currently a primary user id

                        // if it should be
                        if (userId.equals(changePrimaryUserId)) {
                            // add shiny new primary user id certificate
                            log.add(LogType.MSG_MF_PRIMARY_NEW, indent);
                            modifiedPublicKey = PGPPublicKey.removeCertification(
                                    modifiedPublicKey, userId, currentCert);
                            try {
                                PGPSignature newCert = generateUserIdSignature(
                                        getSignatureGenerator(masterSecretKey, cryptoInput),
                                        cryptoInput.getSignatureTime(),
                                        masterPrivateKey, masterPublicKey, userId, true,
                                        masterKeyFlags, masterKeyExpiry);
                                modifiedPublicKey = PGPPublicKey.addCertification(
                                        modifiedPublicKey, userId, newCert);
                            } catch (NfcInteractionNeeded e) {
                                nfcSignOps.addHash(e.hashToSign, e.hashAlgo);
                            }
                            ok = true;
                        }

                        // user id is not primary and is not supposed to be - nothing to do here.

                    }

                    indent -= 1;

                    if (!ok) {
                        log.add(LogType.MSG_MF_ERROR_NOEXIST_PRIMARY, indent);
                        return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                    }
                }

                // Update the secret key ring
                if (modifiedPublicKey != masterPublicKey) {
                    masterSecretKey = PGPSecretKey.replacePublicKey(masterSecretKey, modifiedPublicKey);
                    masterPublicKey = modifiedPublicKey;
                    sKR = PGPSecretKeyRing.insertSecretKey(sKR, masterSecretKey);
                }

            }

            // Check if we were cancelled - again
            if (checkCancelled()) {
                log.add(LogType.MSG_OPERATION_CANCELLED, indent);
                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_CANCELLED, log, null);
            }

            // 4a. For each subkey change, generate new subkey binding certificate
            subProgressPush(50, 60);
            List<SubkeyChange> changeSubKeys = saveParcel.getChangeSubKeys();
            for (int i = 0, j = changeSubKeys.size(); i < j; i++) {

                progress(R.string.progress_modify_subkeychange, (i-1) * (100 / changeSubKeys.size()));
                SaveKeyringParcel.SubkeyChange change = changeSubKeys.get(i);
                log.add(LogType.MSG_MF_SUBKEY_CHANGE,
                        indent, KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()));

                PGPSecretKey sKey = sKR.getSecretKey(change.getSubKeyId());
                if (sKey == null) {
                    log.add(LogType.MSG_MF_ERROR_SUBKEY_MISSING,
                            indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()));
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                }

                if (change.getDummyStrip()) {
                    // IT'S DANGEROUS~
                    // no really, it is. this operation irrevocably removes the private key data from the key
                    sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey());
                    sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
                } else if (change.getMoveKeyToSecurityToken()) {
                    if (checkSecurityTokenCompatibility(sKey, log, indent + 1)) {
                        log.add(LogType.MSG_MF_KEYTOCARD_START, indent + 1,
                                KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()));
                        nfcKeyToCardOps.addSubkey(change.getSubKeyId());
                    } else {
                        // Appropriate log message already set by checkSecurityTokenCompatibility
                        return new PgpEditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
                    }
                } else if (change.getSecurityTokenSerialNo() != null) {
                    // NOTE: Does this code get executed? Or always handled in internalRestricted?
                    if (change.getSecurityTokenSerialNo().length != 16) {
                        log.add(LogType.MSG_MF_ERROR_DIVERT_SERIAL,
                                indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()));
                        return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                    }
                    log.add(LogType.MSG_MF_KEYTOCARD_FINISH, indent + 1,
                            KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()),
                            Hex.toHexString(change.getSecurityTokenSerialNo(), 8, 6));
                    sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey(), change.getSecurityTokenSerialNo());
                    sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
                }



                // This doesn't concern us any further
                if (!change.getRecertify() && (change.getExpiry() == null && change.getFlags() == null)) {
                    continue;
                }

                // expiry must not be in the past
                if (change.getExpiry() != null && change.getExpiry() != 0 &&
                        new Date(change.getExpiry() * 1000).before(new Date())) {
                    log.add(LogType.MSG_MF_ERROR_PAST_EXPIRY,
                            indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()));
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                }

                // if this is the master key, update uid certificates instead
                if (change.getSubKeyId() == masterPublicKey.getKeyID()) {
                    int flags = change.getFlags() == null ? masterKeyFlags : change.getFlags();
                    long expiry = change.getExpiry() == null ? masterKeyExpiry : change.getExpiry();

                    if ((flags & KeyFlags.CERTIFY_OTHER) != KeyFlags.CERTIFY_OTHER) {
                        log.add(LogType.MSG_MF_ERROR_NO_CERTIFY, indent + 1);
                        return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                    }

                    PGPPublicKey pKey =
                            updateMasterCertificates(
                                    masterSecretKey, masterPrivateKey, masterPublicKey,
                                    flags, expiry, cryptoInput,  nfcSignOps, indent, log);
                    if (pKey == null) {
                        // error log entry has already been added by updateMasterCertificates itself
                        return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                    }
                    masterSecretKey = PGPSecretKey.replacePublicKey(sKey, pKey);
                    masterPublicKey = pKey;
                    sKR = PGPSecretKeyRing.insertSecretKey(sKR, masterSecretKey);
                    continue;
                }

                // otherwise, continue working on the public key
                PGPPublicKey pKey = sKey.getPublicKey();

                // keep old flags, or replace with new ones
                int flags = change.getFlags() == null ? readKeyFlags(pKey) : change.getFlags();
                long expiry;
                if (change.getExpiry() == null) {
                    long valid = pKey.getValidSeconds();
                    expiry = valid == 0
                            ? 0
                            : pKey.getCreationTime().getTime() / 1000 + pKey.getValidSeconds();
                } else {
                    expiry = change.getExpiry();
                }

                // drop all old signatures, they will be superseded by the new one
                //noinspection unchecked
                for (PGPSignature sig : new IterableIterator<PGPSignature>(pKey.getSignatures())) {
                    // special case: if there is a revocation, don't use expiry from before
                    if ( (change.getExpiry() == null || change.getExpiry() == 0L)
                            && sig.getSignatureType() == PGPSignature.SUBKEY_REVOCATION) {
                        expiry = 0;
                    }
                    pKey = PGPPublicKey.removeCertification(pKey, sig);
                }

                PGPPrivateKey subPrivateKey;
                if (!isDivertToCard(sKey)) {
                    PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
                            .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
                                    cryptoInput.getPassphrase().getCharArray());
                    subPrivateKey = sKey.extractPrivateKey(keyDecryptor);
                    // super special case: subkey is allowed to sign, but isn't available
                    if (subPrivateKey == null) {
                        log.add(LogType.MSG_MF_ERROR_SUB_STRIPPED,
                                indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()));
                        return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                    }
                } else {
                    subPrivateKey = null;
                }
                try {
                    PGPSignature sig = generateSubkeyBindingSignature(
                            getSignatureGenerator(masterSecretKey, cryptoInput),
                            cryptoInput.getSignatureTime(), masterPublicKey, masterPrivateKey,
                            getSignatureGenerator(sKey, cryptoInput), subPrivateKey,
                            pKey, flags, expiry);

                    // generate and add new signature
                    pKey = PGPPublicKey.addCertification(pKey, sig);
                    sKR = PGPSecretKeyRing.insertSecretKey(sKR, PGPSecretKey.replacePublicKey(sKey, pKey));
                } catch (NfcInteractionNeeded e) {
                    nfcSignOps.addHash(e.hashToSign, e.hashAlgo);
                }

            }
            subProgressPop();

            // 4b. For each subkey revocation, generate new subkey revocation certificate
            subProgressPush(60, 65);
            List<Long> revokeSubKeys = saveParcel.getRevokeSubKeys();
            for (int i = 0, j = revokeSubKeys.size(); i < j; i++) {
                progress(R.string.progress_modify_subkeyrevoke, (i-1) * (100 / revokeSubKeys.size()));
                long revocation = revokeSubKeys.get(i);
                log.add(LogType.MSG_MF_SUBKEY_REVOKE,
                        indent, KeyFormattingUtils.convertKeyIdToHex(revocation));

                PGPSecretKey sKey = sKR.getSecretKey(revocation);
                if (sKey == null) {
                    log.add(LogType.MSG_MF_ERROR_SUBKEY_MISSING,
                            indent+1, KeyFormattingUtils.convertKeyIdToHex(revocation));
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                }
                PGPPublicKey pKey = sKey.getPublicKey();

                // generate and add new signature
                try {
                    PGPSignature sig = generateRevocationSignature(
                            getSignatureGenerator(masterSecretKey, cryptoInput),
                            cryptoInput.getSignatureTime(),
                            masterPublicKey, masterPrivateKey, pKey);

                    pKey = PGPPublicKey.addCertification(pKey, sig);
                    sKR = PGPSecretKeyRing.insertSecretKey(sKR, PGPSecretKey.replacePublicKey(sKey, pKey));
                } catch (NfcInteractionNeeded e) {
                    nfcSignOps.addHash(e.hashToSign, e.hashAlgo);
                }
            }
            subProgressPop();

            // 5. Generate and add new subkeys
            subProgressPush(70, 90);
            List<SubkeyAdd> addSubKeys = saveParcel.getAddSubKeys();
            for (int i = 0, j = addSubKeys.size(); i < j; i++) {
                // Check if we were cancelled - again. This operation is expensive so we do it each loop.
                if (checkCancelled()) {
                    log.add(LogType.MSG_OPERATION_CANCELLED, indent);
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_CANCELLED, log, null);
                }

                progress(R.string.progress_modify_subkeyadd, (i-1) * (100 / addSubKeys.size()));
                SaveKeyringParcel.SubkeyAdd add = addSubKeys.get(i);
                log.add(LogType.MSG_MF_SUBKEY_NEW, indent,
                        KeyFormattingUtils.getAlgorithmInfo(add.getAlgorithm(), add.getKeySize(), add.getCurve()) );

                if (isDivertToCard(masterSecretKey)) {
                    log.add(LogType.MSG_MF_ERROR_DIVERT_NEWSUB, indent +1);
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                }

                if (add.getExpiry() == null) {
                    log.add(LogType.MSG_MF_ERROR_NULL_EXPIRY, indent +1);
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                }

                if (add.getExpiry() > 0L && new Date(add.getExpiry() * 1000).before(new Date())) {
                    log.add(LogType.MSG_MF_ERROR_PAST_EXPIRY, indent +1);
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                }

                // generate a new secret key (privkey only for now)
                subProgressPush(
                    (i-1) * (100 / addSubKeys.size()),
                    i * (100 / addSubKeys.size())
                );
                PGPKeyPair keyPair = createKey(add, cryptoInput.getSignatureTime(), log, indent);
                subProgressPop();
                if (keyPair == null) {
                    log.add(LogType.MSG_MF_ERROR_PGP, indent +1);
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                }

                // add subkey binding signature (making this a sub rather than master key)
                PGPPublicKey pKey = keyPair.getPublicKey();
                try {
                    PGPSignature cert = generateSubkeyBindingSignature(
                            getSignatureGenerator(masterSecretKey, cryptoInput),
                            cryptoInput.getSignatureTime(),
                            masterPublicKey, masterPrivateKey,
                            getSignatureGenerator(pKey, cryptoInput, false), keyPair.getPrivateKey(), pKey,
                            add.getFlags(), add.getExpiry());
                    pKey = PGPPublicKey.addSubkeyBindingCertification(pKey, cert);
                } catch (NfcInteractionNeeded e) {
                    nfcSignOps.addHash(e.hashToSign, e.hashAlgo);
                }

                PGPSecretKey sKey; {
                    PBESecretKeyEncryptor keyEncryptor = buildKeyEncryptorFromPassphrase(cryptoInput.getPassphrase());

                    PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder()
                            .build().get(PgpSecurityConstants.SECRET_KEY_SIGNATURE_CHECKSUM_HASH_ALGO);
                    sKey = new PGPSecretKey(keyPair.getPrivateKey(), pKey, sha1Calc, false, keyEncryptor);
                }

                log.add(LogType.MSG_MF_SUBKEY_NEW_ID,
                        indent+1, KeyFormattingUtils.convertKeyIdToHex(sKey.getKeyID()));

                sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);

            }
            subProgressPop();

            // Check if we were cancelled - again. This operation is expensive so we do it each loop.
            if (checkCancelled()) {
                log.add(LogType.MSG_OPERATION_CANCELLED, indent);
                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_CANCELLED, log, null);
            }

            // 6. If requested, change passphrase
            if (saveParcel.getNewUnlock() != null) {
                progress(R.string.progress_modify_passphrase, 90);
                log.add(LogType.MSG_MF_PASSPHRASE, indent);
                indent += 1;

                sKR = applyNewPassphrase(sKR, masterPublicKey, cryptoInput.getPassphrase(),
                        saveParcel.getNewUnlock().getNewPassphrase(), log, indent);
                if (sKR == null) {
                    // The error has been logged above, just return a bad state
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                }

                indent -= 1;
            }

            // 7. if requested, change PIN and/or Admin PIN on security token
            if (saveParcel.getSecurityTokenPin() != null) {
                progress(R.string.progress_modify_pin, 90);
                log.add(LogType.MSG_MF_PIN, indent);
                indent += 1;

                nfcKeyToCardOps.setPin(saveParcel.getSecurityTokenPin());

                indent -= 1;
            }
            if (saveParcel.getSecurityTokenAdminPin() != null) {
                progress(R.string.progress_modify_admin_pin, 90);
                log.add(LogType.MSG_MF_ADMIN_PIN, indent);
                indent += 1;

                nfcKeyToCardOps.setAdminPin(saveParcel.getSecurityTokenAdminPin());

                indent -= 1;
            }

        } catch (IOException e) {
            Timber.e(e, "encountered IOException while modifying key");
            log.add(LogType.MSG_MF_ERROR_ENCODE, indent+1);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        } catch (PGPException e) {
            Timber.e(e, "encountered pgp error while modifying key");
            log.add(LogType.MSG_MF_ERROR_PGP, indent+1);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        } catch (SignatureException e) {
            Timber.e(e, "encountered SignatureException while modifying key");
            log.add(LogType.MSG_MF_ERROR_SIG, indent+1);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        progress(R.string.progress_done, 100);

        if (!nfcSignOps.isEmpty() && !nfcKeyToCardOps.isEmpty()) {
            log.add(LogType.MSG_MF_ERROR_CONFLICTING_NFC_COMMANDS, indent+1);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        if (!nfcSignOps.isEmpty()) {
            log.add(LogType.MSG_MF_REQUIRE_DIVERT, indent);
            return new PgpEditKeyResult(log, nfcSignOps.build(), cryptoInput);
        }

        if (!nfcKeyToCardOps.isEmpty()) {
            log.add(LogType.MSG_MF_REQUIRE_DIVERT, indent);
            return new PgpEditKeyResult(log, nfcKeyToCardOps.build(), cryptoInput);
        }

        log.add(LogType.MSG_MF_SUCCESS, indent);
        return new PgpEditKeyResult(OperationResult.RESULT_OK, log, new UncachedKeyRing(sKR));

    }

    @Nullable
    private static PBESecretKeyEncryptor buildKeyEncryptorFromPassphrase(Passphrase passphrase) throws PGPException {
        if (passphrase == null || passphrase.isEmpty()) {
            return null;
        }

        PGPDigestCalculator encryptorHashCalc = new JcaPGPDigestCalculatorProviderBuilder()
                .build()
                .get(PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_HASH_ALGO);
        return new JcePBESecretKeyEncryptorBuilder(
                PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO,
                encryptorHashCalc, PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT)
                .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME)
                .build(passphrase.getCharArray());
    }

    /** This method does the actual modifications in a keyring just like internal, except it
     * supports only the subset of operations which require no passphrase, and will error
     * otherwise.
     */
    private PgpEditKeyResult internalRestricted(PGPSecretKeyRing sKR, SaveKeyringParcel saveParcel,
                                                OperationLog log, int indent) {

        progress(R.string.progress_modify, 0);

        // Make sure the saveParcel includes only operations available without passphrase!
        if (!isParcelRestrictedOnly(saveParcel)) {
            log.add(LogType.MSG_MF_ERROR_RESTRICTED, indent);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        // Check if we were cancelled
        if (checkCancelled()) {
            log.add(LogType.MSG_OPERATION_CANCELLED, indent);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_CANCELLED, log, null);
        }

        // The only operation we can do here:
        // 4a. Strip secret keys, or change their protection mode (stripped/divert-to-card)
        subProgressPush(50, 60);
        List<SubkeyChange> changeSubKeys = saveParcel.getChangeSubKeys();
        for (int i = 0, j = changeSubKeys.size(); i < j; i++) {
            progress(R.string.progress_modify_subkeychange, (i - 1) * (100 / changeSubKeys.size()));
            SaveKeyringParcel.SubkeyChange change = changeSubKeys.get(i);
            log.add(LogType.MSG_MF_SUBKEY_CHANGE,
                    indent, KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()));

            PGPSecretKey sKey = sKR.getSecretKey(change.getSubKeyId());
            if (sKey == null) {
                log.add(LogType.MSG_MF_ERROR_SUBKEY_MISSING,
                        indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()));
                return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
            }

            if (change.getDummyStrip() || change.getSecurityTokenSerialNo() != null) {
                // IT'S DANGEROUS~
                // no really, it is. this operation irrevocably removes the private key data from the key
                if (change.getDummyStrip()) {
                    sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey());
                } else {
                    // the serial number must be 16 bytes in length
                    if (change.getSecurityTokenSerialNo().length != 16) {
                        log.add(LogType.MSG_MF_ERROR_DIVERT_SERIAL,
                                indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()));
                        return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                    }
                    log.add(LogType.MSG_MF_KEYTOCARD_FINISH, indent + 1,
                            KeyFormattingUtils.convertKeyIdToHex(change.getSubKeyId()),
                            Hex.toHexString(change.getSecurityTokenSerialNo(), 8, 6));
                    sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey(), change.getSecurityTokenSerialNo());
                }
                sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
            }

        }

        // And we're done!
        progress(R.string.progress_done, 100);
        log.add(LogType.MSG_MF_SUCCESS, indent);
        return new PgpEditKeyResult(OperationResult.RESULT_OK, log, new UncachedKeyRing(sKR));

    }


    public PgpEditKeyResult modifyKeyRingPassphrase(CanonicalizedSecretKeyRing wsKR,
                                                    CryptoInputParcel cryptoInput,
                                                    ChangeUnlockParcel changeUnlockParcel) {

        OperationLog log = new OperationLog();
        int indent = 0;

        Long masterKeyId = changeUnlockParcel.getMasterKeyId();
        if (masterKeyId == null || masterKeyId != wsKR.getMasterKeyId()) {
            log.add(LogType.MSG_MF_ERROR_KEYID, indent);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        log.add(LogType.MSG_MF, indent,
                KeyFormattingUtils.convertKeyIdToHex(wsKR.getMasterKeyId()));
        indent += 1;
        progress(R.string.progress_building_key, 0);

        // We work on bouncycastle object level here
        PGPSecretKeyRing sKR = wsKR.getRing();
        PGPSecretKey masterSecretKey = sKR.getSecretKey();
        PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();
        // Make sure the fingerprint matches
        if (changeUnlockParcel.getFingerprint()== null || !Arrays.equals(changeUnlockParcel.getFingerprint(),
                masterSecretKey.getPublicKey().getFingerprint())) {
            log.add(LogType.MSG_MF_ERROR_FINGERPRINT, indent);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        // Find the first unstripped secret key
        PGPSecretKey nonDummy = firstNonDummySecretKeyID(sKR);
        if(nonDummy == null) {
            log.add(OperationResult.LogType.MSG_MF_ERROR_ALL_KEYS_STRIPPED, indent);
            return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
        }

        if (!cryptoInput.hasPassphraseForSubkey(nonDummy.getKeyID())) {
            log.add(LogType.MSG_MF_REQUIRE_PASSPHRASE, indent);

            return new PgpEditKeyResult(log, RequiredInputParcel.createRequiredSignPassphrase(
                    masterSecretKey.getKeyID(), nonDummy.getKeyID(),
                    cryptoInput.getSignatureTime()), cryptoInput);
        } else {
            progress(R.string.progress_modify_passphrase, 50);
            log.add(LogType.MSG_MF_PASSPHRASE, indent);
            indent += 1;

            try {
                sKR = applyNewPassphrase(sKR, masterPublicKey, cryptoInput.getPassphrase(),
                        changeUnlockParcel.getNewPassphrase(), log, indent);
                if (sKR == null) {
                    // The error has been logged above, just return a bad state
                    return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
                }
            } catch (PGPException e) {
                throw new UnsupportedOperationException("Failed to build encryptor/decryptor!");
            }

            indent -= 1;
            progress(R.string.progress_done, 100);
            log.add(LogType.MSG_MF_SUCCESS, indent);
            return new PgpEditKeyResult(OperationResult.RESULT_OK, log, new UncachedKeyRing(sKR));
        }
    }

    private static PGPSecretKey firstNonDummySecretKeyID(PGPSecretKeyRing secRing) {
        Iterator<PGPSecretKey> secretKeyIterator = secRing.getSecretKeys();

        while(secretKeyIterator.hasNext()) {
            PGPSecretKey secretKey = secretKeyIterator.next();
            if(!isDummy(secretKey)){
                return secretKey;
            }
        }
        return null;
    }

    /** This method returns true iff the provided keyring has a local direct key signature
     * with notation data.
     */
    private static boolean hasNotationData(PGPSecretKeyRing sKR) {
        // noinspection unchecked
        Iterator<PGPSignature> sigs = sKR.getPublicKey().getKeySignatures();
        while (sigs.hasNext()) {
            WrappedSignature sig = new WrappedSignature(sigs.next());
            if (sig.getSignatureType() == PGPSignature.DIRECT_KEY
                    && sig.isLocal() && !sig.getNotation().isEmpty()) {
                return true;
            }
        }
        return false;
    }

    private static PGPSecretKeyRing applyNewPassphrase(
            PGPSecretKeyRing sKR,
            PGPPublicKey masterPublicKey,
            Passphrase passphrase,
            Passphrase newPassphrase,
            OperationLog log, int indent) throws PGPException {

        PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
                Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.getCharArray());
        // Build key encryptor based on new passphrase
        PBESecretKeyEncryptor keyEncryptor = buildKeyEncryptorFromPassphrase(newPassphrase);

        boolean keysModified = false;

        for (PGPSecretKey sKey : new IterableIterator<>(sKR.getSecretKeys())) {
            log.add(LogType.MSG_MF_PASSPHRASE_KEY, indent,
                    KeyFormattingUtils.convertKeyIdToHex(sKey.getKeyID()));

            boolean ok = false;

            try {
                // try to set new passphrase
                sKey = PGPSecretKey.copyWithNewPassword(sKey, keyDecryptor, keyEncryptor);
                ok = true;
            } catch (PGPException e) {

                // if the master key failed && it's not stripped, error!
                if (sKey.getKeyID() == masterPublicKey.getKeyID() && !isDummy(sKey)) {
                    log.add(LogType.MSG_MF_ERROR_PASSPHRASE_MASTER, indent+1);
                    return null;
                }

                // being in here means decrypt failed, likely due to a bad passphrase try
                // again with an empty passphrase, maybe we can salvage this
                try {
                    log.add(LogType.MSG_MF_PASSPHRASE_EMPTY_RETRY, indent+1);
                    PBESecretKeyDecryptor emptyDecryptor =
                            new JcePBESecretKeyDecryptorBuilder().setProvider(
                                    Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
                    sKey = PGPSecretKey.copyWithNewPassword(sKey, emptyDecryptor, keyEncryptor);
                    ok = true;
                } catch (PGPException e2) {
                    // non-fatal but not ok, handled below
                }
            }

            if (!ok) {
                // for a subkey, it's merely a warning
                log.add(LogType.MSG_MF_PASSPHRASE_FAIL, indent+1,
                        KeyFormattingUtils.convertKeyIdToHex(sKey.getKeyID()));
                continue;
            }

            sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
            keysModified = true;
        }

        if(!keysModified) {
            // no passphrase was changed
            log.add(LogType.MSG_MF_ERROR_PASSPHRASES_UNCHANGED, indent+1);
            return null;
        }

        return sKR;

    }

    /** Update all (non-revoked) uid signatures with new flags and expiry time. */
    private PGPPublicKey updateMasterCertificates(
            PGPSecretKey masterSecretKey, PGPPrivateKey masterPrivateKey,
            PGPPublicKey masterPublicKey,
            int flags, long expiry,
            CryptoInputParcel cryptoInput,
            SecurityTokenSignOperationsBuilder nfcSignOps,
            int indent, OperationLog log)
            throws PGPException, IOException, SignatureException {

        // keep track if we actually changed one
        boolean ok = false;
        log.add(LogType.MSG_MF_MASTER, indent);
        indent += 1;

        PGPPublicKey modifiedPublicKey = masterPublicKey;

        // we work on the modifiedPublicKey here, to respect new or newly revoked uids
        // noinspection unchecked
        for (String userId : new IterableIterator<String>(modifiedPublicKey.getUserIDs())) {
            boolean isRevoked = false;
            PGPSignature currentCert = null;
            // noinspection unchecked
            for (PGPSignature cert : new IterableIterator<PGPSignature>(
                    modifiedPublicKey.getSignaturesForID(userId))) {
                if (cert.getKeyID() != masterPublicKey.getKeyID()) {
                    // foreign certificate?! error error error
                    log.add(LogType.MSG_MF_ERROR_INTEGRITY, indent);
                    return null;
                }
                // we know from canonicalization that if there is any revocation here, it
                // is valid and not superseded by a newer certification.
                if (cert.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION) {
                    isRevoked = true;
                    continue;
                }
                // we know from canonicalization that there is only one binding
                // certification here, so we can just work with the first one.
                if (cert.getSignatureType() == PGPSignature.NO_CERTIFICATION ||
                        cert.getSignatureType() == PGPSignature.CASUAL_CERTIFICATION ||
                        cert.getSignatureType() == PGPSignature.POSITIVE_CERTIFICATION ||
                        cert.getSignatureType() == PGPSignature.DEFAULT_CERTIFICATION) {
                    currentCert = cert;
                }
            }

            if (currentCert == null) {
                // no certificate found?! error error error
                log.add(LogType.MSG_MF_ERROR_INTEGRITY, indent);
                return null;
            }

            // we definitely should not update certifications of revoked keys, so just leave it.
            if (isRevoked) {
                continue;
            }

            // add shiny new user id certificate
            boolean isPrimary = currentCert.getHashedSubPackets() != null &&
                    currentCert.getHashedSubPackets().isPrimaryUserID();
            modifiedPublicKey = PGPPublicKey.removeCertification(
                    modifiedPublicKey, userId, currentCert);
            try {
                PGPSignature newCert = generateUserIdSignature(
                        getSignatureGenerator(masterSecretKey, cryptoInput),
                        cryptoInput.getSignatureTime(),
                        masterPrivateKey, masterPublicKey, userId, isPrimary, flags, expiry);
                modifiedPublicKey = PGPPublicKey.addCertification(
                        modifiedPublicKey, userId, newCert);
            } catch (NfcInteractionNeeded e) {
                nfcSignOps.addHash(e.hashToSign, e.hashAlgo);
            }
            ok = true;

        }

        if (!ok) {
            // might happen, theoretically, if there is a key with no uid..
            log.add(LogType.MSG_MF_ERROR_MASTER_NONE, indent);
            return null;
        }

        return modifiedPublicKey;

    }

    static PGPSignatureGenerator getSignatureGenerator(
            PGPSecretKey secretKey, CryptoInputParcel cryptoInput) {

        S2K s2k = secretKey.getS2K();
        boolean isDivertToCard = s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K
                && s2k.getProtectionMode() == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD;

        return getSignatureGenerator(secretKey.getPublicKey(), cryptoInput, isDivertToCard);
    }

    static PGPSignatureGenerator getSignatureGenerator(
            PGPPublicKey pKey, CryptoInputParcel cryptoInput, boolean divertToCard) {

        PGPContentSignerBuilder builder;
        if (divertToCard) {
            // use synchronous "NFC based" SignerBuilder
            builder = new NfcSyncPGPContentSignerBuilder(
                    pKey.getAlgorithm(), pKey.getKeyID(),
                    true, PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO,
                    cryptoInput.getCryptoData())
                    .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
        } else {
            // content signer based on signing key algorithm and chosen hash algorithm
            builder = new JcaPGPContentSignerBuilder(
                    pKey.getAlgorithm(), PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO)
                    .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
        }

        return new PGPSignatureGenerator(builder);

    }

    private static PGPSignatureSubpacketGenerator generateHashedSelfSigSubpackets(
            Date creationTime, PGPPublicKey pKey, boolean primary, int flags, long expiry
    ) {

        PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
        {
            /*
             * From RFC about critical subpackets:
             * If a subpacket is encountered that is
             * marked critical but is unknown to the evaluating software, the
             * evaluator SHOULD consider the signature to be in error.
             * An evaluator may "recognize" a subpacket, but not implement it.  The
             * purpose of the critical bit is to allow the signer to tell an
             * evaluator that it would prefer a new, unknown feature to generate an
             * error than be ignored.
             */
            /* non-critical subpackets: */
            hashedPacketsGen.setPreferredSymmetricAlgorithms(false,
                    PgpSecurityConstants.PREFERRED_SYMMETRIC_ALGORITHMS);
            hashedPacketsGen.setPreferredHashAlgorithms(false,
                    PgpSecurityConstants.PREFERRED_HASH_ALGORITHMS);
            hashedPacketsGen.setPreferredCompressionAlgorithms(false,
                    PgpSecurityConstants.PREFERRED_COMPRESSION_ALGORITHMS);
            hashedPacketsGen.setPrimaryUserID(false, primary);

            /* critical subpackets: we consider those important for a modern pgp implementation */
            hashedPacketsGen.setSignatureCreationTime(true, creationTime);
            // Request that senders add the MDC to the message (authenticate unsigned messages)
            hashedPacketsGen.setFeature(true, Features.FEATURE_MODIFICATION_DETECTION);
            hashedPacketsGen.setKeyFlags(true, flags);
            if (expiry > 0) {
                hashedPacketsGen.setKeyExpirationTime(
                        true, expiry - pKey.getCreationTime().getTime() / 1000);
            }
        }

        return hashedPacketsGen;
    }

    private static PGPSignature generateUserIdSignature(
            PGPSignatureGenerator sGen, Date creationTime,
            PGPPrivateKey masterPrivateKey, PGPPublicKey pKey, String userId, boolean primary,
            int flags, long expiry)
            throws IOException, PGPException, SignatureException {

        PGPSignatureSubpacketGenerator hashedPacketsGen =
                generateHashedSelfSigSubpackets(creationTime, pKey, primary, flags, expiry);
        sGen.setHashedSubpackets(hashedPacketsGen.generate());
        sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
        return sGen.generateCertification(userId, pKey);
    }

    private static PGPSignature generateUserAttributeSignature(
            PGPSignatureGenerator sGen, Date creationTime,
            PGPPrivateKey masterPrivateKey, PGPPublicKey pKey,
            PGPUserAttributeSubpacketVector vector,
            int flags, long expiry)
                throws IOException, PGPException, SignatureException {

        PGPSignatureSubpacketGenerator hashedPacketsGen =
                generateHashedSelfSigSubpackets(creationTime, pKey, false, flags, expiry);
        sGen.setHashedSubpackets(hashedPacketsGen.generate());
        sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
        return sGen.generateCertification(vector, pKey);
    }

    private static PGPSignature generateRevocationSignature(
            PGPSignatureGenerator sGen, Date creationTime,
            PGPPrivateKey masterPrivateKey, PGPPublicKey pKey, String userId)

        throws IOException, PGPException, SignatureException {
        PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator();
        // GnuPG adds an empty NO_REASON revocation reason packet, so we do the same
        // see https://lists.gnupg.org/pipermail/gnupg-devel/2017-April/032779.html
        subHashedPacketsGen.setRevocationReason(false, RevocationReasonTags.NO_REASON, "");
        subHashedPacketsGen.setSignatureCreationTime(true, creationTime);
        sGen.setHashedSubpackets(subHashedPacketsGen.generate());
        sGen.init(PGPSignature.CERTIFICATION_REVOCATION, masterPrivateKey);
        return sGen.generateCertification(userId, pKey);
    }

    private static PGPSignature generateRevocationSignature(
            PGPSignatureGenerator sGen, Date creationTime,
            PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey, PGPPublicKey pKey)
            throws IOException, PGPException, SignatureException {

        PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator();
        // we use the tag NO_REASON since gnupg does not care about the tag while verifying
        // signatures with a revoked key, the warning is the same
        subHashedPacketsGen.setRevocationReason(true, RevocationReasonTags.NO_REASON, "");
        subHashedPacketsGen.setSignatureCreationTime(true, creationTime);
        sGen.setHashedSubpackets(subHashedPacketsGen.generate());
        // Generate key revocation or subkey revocation, depending on master/subkey-ness
        if (masterPublicKey.getKeyID() == pKey.getKeyID()) {
            sGen.init(PGPSignature.KEY_REVOCATION, masterPrivateKey);
            return sGen.generateCertification(masterPublicKey);
        } else {
            sGen.init(PGPSignature.SUBKEY_REVOCATION, masterPrivateKey);
            return sGen.generateCertification(masterPublicKey, pKey);
        }
    }

    static PGPSignature generateSubkeyBindingSignature(
            PGPSignatureGenerator sGen, Date creationTime,
            PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey,
            PGPSignatureGenerator subSigGen, PGPPrivateKey subPrivateKey, PGPPublicKey pKey,
            int flags, long expiry)
            throws IOException, PGPException, SignatureException {

        PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator();

        // If this key can sign, we need a primary key binding signature
        if ((flags & KeyFlags.SIGN_DATA) > 0) {
            // cross-certify signing keys
            PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator();
            subHashedPacketsGen.setSignatureCreationTime(false, creationTime);
            subSigGen.init(PGPSignature.PRIMARYKEY_BINDING, subPrivateKey);
            subSigGen.setHashedSubpackets(subHashedPacketsGen.generate());
            PGPSignature certification = subSigGen.generateCertification(masterPublicKey, pKey);
            unhashedPacketsGen.setEmbeddedSignature(true, certification);
        }

        PGPSignatureSubpacketGenerator hashedPacketsGen;
        {
            hashedPacketsGen = new PGPSignatureSubpacketGenerator();
            hashedPacketsGen.setSignatureCreationTime(true, creationTime);
            hashedPacketsGen.setKeyFlags(true, flags);
            if (expiry > 0) {
                hashedPacketsGen.setKeyExpirationTime(true,
                        expiry - pKey.getCreationTime().getTime() / 1000);
            }
        }

        sGen.init(PGPSignature.SUBKEY_BINDING, masterPrivateKey);
        sGen.setHashedSubpackets(hashedPacketsGen.generate());
        sGen.setUnhashedSubpackets(unhashedPacketsGen.generate());

        return sGen.generateCertification(masterPublicKey, pKey);

    }

    /** Returns all flags valid for this key.
     *
     * This method does not do any validity checks on the signature, so it should not be used on
     * a non-canonicalized key!
     *
     */
    private static int readKeyFlags(PGPPublicKey key) {
        int flags = 0;
        //noinspection unchecked
        for(PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
            if (sig.getHashedSubPackets() == null) {
                continue;
            }
            flags |= sig.getHashedSubPackets().getKeyFlags();
        }
        return flags;
    }

    private static boolean isDummy(PGPSecretKey secretKey) {
        S2K s2k = secretKey.getS2K();
        return s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K
                && s2k.getProtectionMode() != S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD;
    }

    private static boolean isDivertToCard(PGPSecretKey secretKey) {
        S2K s2k = secretKey.getS2K();
        return s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K
                && s2k.getProtectionMode() == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD;
    }

    private static boolean checkSecurityTokenCompatibility(PGPSecretKey key, OperationLog log, int indent) {

        final PGPPublicKey publicKey = key.getPublicKey();
        ASN1ObjectIdentifier curve;

        switch (publicKey.getAlgorithm()) {
            case PublicKeyAlgorithmTags.RSA_ENCRYPT:
            case PublicKeyAlgorithmTags.RSA_SIGN:
            case PublicKeyAlgorithmTags.RSA_GENERAL:
                // Key size must be at least 2048 since OpenPGP card specification 3.x
                if (publicKey.getBitStrength() < 2048) {
                    log.add(LogType.MSG_MF_ERROR_BAD_SECURITY_TOKEN_RSA_KEY_SIZE, indent + 1);
                    return false;
                }
                break;

            case PublicKeyAlgorithmTags.ECDH:
                curve = ((ECDHPublicBCPGKey)(publicKey.getPublicKeyPacket().getKey())).getCurveOID();
                if (!curve.equals(NISTNamedCurves.getOID("P-256")) &&
                        !curve.equals(NISTNamedCurves.getOID("P-384")) &&
                        !curve.equals(NISTNamedCurves.getOID("P-521"))) {
                    log.add(LogType.MSG_MF_ERROR_BAD_SECURITY_TOKEN_CURVE, indent + 1);
                    return false;
                }
                break;

            case PublicKeyAlgorithmTags.ECDSA:
                curve = ((ECDSAPublicBCPGKey)(publicKey.getPublicKeyPacket().getKey())).getCurveOID();
                if (!curve.equals(NISTNamedCurves.getOID("P-256")) &&
                        !curve.equals(NISTNamedCurves.getOID("P-384")) &&
                        !curve.equals(NISTNamedCurves.getOID("P-521"))) {
                    log.add(LogType.MSG_MF_ERROR_BAD_SECURITY_TOKEN_CURVE, indent + 1);
                    return false;
                }
                break;

            default:
                log.add(LogType.MSG_MF_ERROR_BAD_SECURITY_TOKEN_ALGO, indent + 1);
                return false;
        }

        // Secret key parts must be available
        if (isDivertToCard(key) || isDummy(key)) {
            log.add(LogType.MSG_MF_ERROR_BAD_SECURITY_TOKEN_STRIPPED, indent + 1);
            return false;
        }

        return true;
    }

    /** Returns true iff this parcel does not contain any operations which require a passphrase. */
    private static boolean isParcelRestrictedOnly(SaveKeyringParcel saveKeyringParcel) {
        if (saveKeyringParcel.getNewUnlock() != null
                || !saveKeyringParcel.getAddUserIds().isEmpty()
                || !saveKeyringParcel.getAddUserAttribute().isEmpty()
                || !saveKeyringParcel.getAddSubKeys().isEmpty()
                || saveKeyringParcel.getChangePrimaryUserId() != null
                || !saveKeyringParcel.getRevokeUserIds().isEmpty()
                || !saveKeyringParcel.getRevokeSubKeys().isEmpty()) {
            return false;
        }

        for (SubkeyChange change : saveKeyringParcel.getChangeSubKeys()) {
            if (change.getRecertify() || change.getFlags() != null || change.getExpiry() != null
                    || change.getMoveKeyToSecurityToken()) {
                return false;
            }
        }

        return true;
    }

    private static boolean isParcelEmpty(SaveKeyringParcel saveKeyringParcel) {
        return isParcelRestrictedOnly(saveKeyringParcel) && saveKeyringParcel.getChangeSubKeys().isEmpty();
    }

}
