/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package libcore.javax.crypto;

import java.security.AlgorithmParameters;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.Provider;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import junit.framework.TestCase;
import libcore.java.security.StandardNames;

public final class CipherTest extends TestCase {

    private static abstract class MockProvider extends Provider {
        public MockProvider(String name) {
            super(name, 1.0, "Mock provider used for testing");
            setup();
        }

        public abstract void setup();
    }

    public void testCipher_getInstance_SuppliedProviderNotRegistered_Success() throws Exception {
        Provider mockProvider = new MockProvider("MockProvider") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
            }
        };

        {
            Cipher c = Cipher.getInstance("FOO", mockProvider);
            c.init(Cipher.ENCRYPT_MODE, new MockKey());
            assertEquals(mockProvider, c.getProvider());
        }
    }

    public void testCipher_getInstance_DoesNotSupportKeyClass_Success() throws Exception {
        Provider mockProvider = new MockProvider("MockProvider") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
                put("Cipher.FOO SupportedKeyClasses", "None");
            }
        };

        Security.addProvider(mockProvider);
        try {
            Cipher c = Cipher.getInstance("FOO", mockProvider);
            c.init(Cipher.ENCRYPT_MODE, new MockKey());
            assertEquals(mockProvider, c.getProvider());
        } finally {
            Security.removeProvider(mockProvider.getName());
        }
    }

    public void testCipher_getInstance_SuppliedProviderNotRegistered_MultipartTransform_Success()
            throws Exception {
        Provider mockProvider = new MockProvider("MockProvider") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
            }
        };

        {
            Cipher c = Cipher.getInstance("FOO/FOO/FOO", mockProvider);
            c.init(Cipher.ENCRYPT_MODE, new MockKey());
            assertEquals(mockProvider, c.getProvider());
        }
    }

    public void testCipher_getInstance_OnlyUsesSpecifiedProvider_SameNameAndClass_Success()
            throws Exception {
        Provider mockProvider = new MockProvider("MockProvider") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
            }
        };

        Security.addProvider(mockProvider);
        try {
            {
                Provider mockProvider2 = new MockProvider("MockProvider") {
                    public void setup() {
                        put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
                    }
                };
                Cipher c = Cipher.getInstance("FOO", mockProvider2);
                assertEquals(mockProvider2, c.getProvider());
            }
        } finally {
            Security.removeProvider(mockProvider.getName());
        }
    }

    public void testCipher_getInstance_DelayedInitialization_KeyType() throws Exception {
        Provider mockProviderSpecific = new MockProvider("MockProviderSpecific") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.SpecificKeyTypes.class.getName());
                put("Cipher.FOO SupportedKeyClasses", MockKey.class.getName());
            }
        };
        Provider mockProviderSpecific2 = new MockProvider("MockProviderSpecific2") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.SpecificKeyTypes2.class.getName());
                put("Cipher.FOO SupportedKeyClasses", MockKey2.class.getName());
            }
        };
        Provider mockProviderAll = new MockProvider("MockProviderAll") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
            }
        };

        Security.addProvider(mockProviderSpecific);
        Security.addProvider(mockProviderSpecific2);
        Security.addProvider(mockProviderAll);

        try {
            {
                System.out.println(Arrays.deepToString(Security.getProviders("Cipher.FOO")));
                Cipher c = Cipher.getInstance("FOO");
                c.init(Cipher.ENCRYPT_MODE, new MockKey());
                assertEquals(mockProviderSpecific, c.getProvider());

                try {
                    c.init(Cipher.ENCRYPT_MODE, new MockKey2());
                    assertEquals(mockProviderSpecific2, c.getProvider());
                    if (StandardNames.IS_RI) {
                        fail("RI was broken before; fix tests now that it works!");
                    }
                } catch (InvalidKeyException e) {
                    if (!StandardNames.IS_RI) {
                        fail("Non-RI should select the right provider");
                    }
                }
            }

            {
                Cipher c = Cipher.getInstance("FOO");
                c.init(Cipher.ENCRYPT_MODE, new Key() {
                    @Override
                    public String getAlgorithm() {
                        throw new UnsupportedOperationException("not implemented");
                    }

                    @Override
                    public String getFormat() {
                        throw new UnsupportedOperationException("not implemented");
                    }

                    @Override
                    public byte[] getEncoded() {
                        throw new UnsupportedOperationException("not implemented");
                    }
                });
                assertEquals(mockProviderAll, c.getProvider());
            }

            {
                Cipher c = Cipher.getInstance("FOO");
                assertEquals(mockProviderSpecific, c.getProvider());
            }
        } finally {
            Security.removeProvider(mockProviderSpecific.getName());
            Security.removeProvider(mockProviderSpecific2.getName());
            Security.removeProvider(mockProviderAll.getName());
        }
    }

    public void testCipher_getInstance_CorrectPriority_AlgorithmOnlyFirst() throws Exception {
        Provider mockProviderOnlyAlgorithm = new MockProvider("MockProviderOnlyAlgorithm") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
            }
        };
        Provider mockProviderFullTransformSpecified = new MockProvider("MockProviderFull") {
            public void setup() {
                put("Cipher.FOO/FOO/FOO", MockCipherSpi.AllKeyTypes.class.getName());
            }
        };

        Security.addProvider(mockProviderOnlyAlgorithm);
        Security.addProvider(mockProviderFullTransformSpecified);
        try {
            Cipher c = Cipher.getInstance("FOO/FOO/FOO");
            assertEquals(mockProviderOnlyAlgorithm, c.getProvider());
        } finally {
            Security.removeProvider(mockProviderOnlyAlgorithm.getName());
            Security.removeProvider(mockProviderFullTransformSpecified.getName());
        }
    }

    public void testCipher_getInstance_CorrectPriority_FullTransformFirst() throws Exception {
        Provider mockProviderOnlyAlgorithm = new MockProvider("MockProviderOnlyAlgorithm") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
            }
        };
        Provider mockProviderFullTransformSpecified = new MockProvider("MockProviderFull") {
            public void setup() {
                put("Cipher.FOO/FOO/FOO", MockCipherSpi.AllKeyTypes.class.getName());
            }
        };

        Security.addProvider(mockProviderFullTransformSpecified);
        Security.addProvider(mockProviderOnlyAlgorithm);
        try {
            Cipher c = Cipher.getInstance("FOO/FOO/FOO");
            assertEquals(mockProviderFullTransformSpecified, c.getProvider());
        } finally {
            Security.removeProvider(mockProviderOnlyAlgorithm.getName());
            Security.removeProvider(mockProviderFullTransformSpecified.getName());
        }
    }

    public void testCipher_getInstance_CorrectPriority_AliasedAlgorithmFirst() throws Exception {
        Provider mockProviderAliasedAlgorithm = new MockProvider("MockProviderAliasedAlgorithm") {
            public void setup() {
                put("Cipher.BAR", MockCipherSpi.AllKeyTypes.class.getName());
                put("Alg.Alias.Cipher.FOO", "BAR");
            }
        };
        Provider mockProviderAlgorithmOnly = new MockProvider("MockProviderAlgorithmOnly") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
            }
        };

        Security.addProvider(mockProviderAliasedAlgorithm);
        Security.addProvider(mockProviderAlgorithmOnly);
        try {
            Cipher c = Cipher.getInstance("FOO/FOO/FOO");
            assertEquals(mockProviderAliasedAlgorithm, c.getProvider());
        } finally {
            Security.removeProvider(mockProviderAliasedAlgorithm.getName());
            Security.removeProvider(mockProviderAlgorithmOnly.getName());
        }
    }

    public void testCipher_getInstance_WrongType_Failure() throws Exception {
        Provider mockProviderInvalid = new MockProvider("MockProviderInvalid") {
            public void setup() {
                put("Cipher.FOO", Object.class.getName());
            }
        };

        Security.addProvider(mockProviderInvalid);
        try {
            Cipher c = Cipher.getInstance("FOO");
            c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(new byte[16], "FOO"));
            fail("Should not find any matching providers; found: " + c);
        } catch (ClassCastException expected) {
        } finally {
            Security.removeProvider(mockProviderInvalid.getName());
        }
    }

    public void testCipher_init_CallsInitWithParams_AlgorithmParameterSpec() throws Exception {
        Provider mockProviderRejects = new MockProvider("MockProviderRejects") {
            public void setup() {
                put("Cipher.FOO",
                        MockCipherSpi.MustInitWithAlgorithmParameterSpec_RejectsAll.class.getName());
                put("Cipher.FOO SupportedKeyClasses", MockKey.class.getName());
            }
        };
        Provider mockProviderAccepts = new MockProvider("MockProviderAccepts") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
                put("Cipher.FOO SupportedKeyClasses", MockKey.class.getName());
            }
        };

        Security.addProvider(mockProviderRejects);
        Security.addProvider(mockProviderAccepts);
        try {
            Cipher c = Cipher.getInstance("FOO");
            c.init(Cipher.ENCRYPT_MODE, new MockKey(), new IvParameterSpec(new byte[12]));
            assertEquals(mockProviderAccepts, c.getProvider());
        } finally {
            Security.removeProvider(mockProviderRejects.getName());
            Security.removeProvider(mockProviderAccepts.getName());
        }
    }

    public void testCipher_init_CallsInitWithParams_AlgorithmParameters() throws Exception {
        Provider mockProviderRejects = new MockProvider("MockProviderRejects") {
            public void setup() {
                put("Cipher.FOO",
                        MockCipherSpi.MustInitWithAlgorithmParameters_RejectsAll.class.getName());
                put("Cipher.FOO SupportedKeyClasses", MockKey.class.getName());
            }
        };
        Provider mockProviderAccepts = new MockProvider("MockProviderAccepts") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
                put("Cipher.FOO SupportedKeyClasses", MockKey.class.getName());
            }
        };

        Security.addProvider(mockProviderRejects);
        Security.addProvider(mockProviderAccepts);
        try {
            Cipher c = Cipher.getInstance("FOO");
            c.init(Cipher.ENCRYPT_MODE, new MockKey(), AlgorithmParameters.getInstance("AES"));
            assertEquals(mockProviderAccepts, c.getProvider());
        } finally {
            Security.removeProvider(mockProviderRejects.getName());
            Security.removeProvider(mockProviderAccepts.getName());
        }
    }

    public void testCipher_init_CallsInitIgnoresRuntimeException() throws Exception {
        Provider mockProviderRejects = new MockProvider("MockProviderRejects") {
            public void setup() {
                put("Cipher.FOO",
                        MockCipherSpi.MustInitWithAlgorithmParameters_ThrowsNull.class.getName());
                put("Cipher.FOO SupportedKeyClasses", MockKey.class.getName());
            }
        };
        Provider mockProviderAccepts = new MockProvider("MockProviderAccepts") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
                put("Cipher.FOO SupportedKeyClasses", MockKey.class.getName());
            }
        };

        Security.addProvider(mockProviderRejects);
        Security.addProvider(mockProviderAccepts);
        try {
            Cipher c = Cipher.getInstance("FOO");
            c.init(Cipher.ENCRYPT_MODE, new MockKey(), AlgorithmParameters.getInstance("AES"));
            assertEquals(mockProviderAccepts, c.getProvider());
        } finally {
            Security.removeProvider(mockProviderRejects.getName());
            Security.removeProvider(mockProviderAccepts.getName());
        }
    }

    public void testCipher_init_CallsInitWithMode() throws Exception {
        Provider mockProviderOnlyEncrypt = new MockProvider("MockProviderOnlyEncrypt") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.MustInitForEncryptModeOrRejects.class.getName());
                put("Cipher.FOO SupportedKeyClasses", MockKey.class.getName());
            }
        };
        Provider mockProviderAcceptsAll = new MockProvider("MockProviderAcceptsAll") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
                put("Cipher.FOO SupportedKeyClasses", MockKey.class.getName());
            }
        };

        Security.addProvider(mockProviderOnlyEncrypt);
        Security.addProvider(mockProviderAcceptsAll);
        try {
            {
                Cipher c = Cipher.getInstance("FOO");
                c.init(Cipher.DECRYPT_MODE, new MockKey(), AlgorithmParameters.getInstance("AES"));
                assertEquals(mockProviderAcceptsAll, c.getProvider());
            }

            {
                Cipher c = Cipher.getInstance("FOO");
                c.init(Cipher.ENCRYPT_MODE, new MockKey(), AlgorithmParameters.getInstance("AES"));
                assertEquals(mockProviderOnlyEncrypt, c.getProvider());
            }
        } finally {
            Security.removeProvider(mockProviderOnlyEncrypt.getName());
            Security.removeProvider(mockProviderAcceptsAll.getName());
        }
    }

    /**
     * Several exceptions can be thrown by init. Check that in this case we throw the right one,
     * as the error could fall under the umbrella of other exceptions.
     * http://b/18987633
     */
    public void testCipher_init_DoesNotSupportKeyClass_throwsInvalidKeyException()
            throws Exception {
        Provider mockProvider = new MockProvider("MockProvider") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
                put("Cipher.FOO SupportedKeyClasses", "none");
            }
        };

        Security.addProvider(mockProvider);
        try {
            Cipher c = Cipher.getInstance("FOO");
            c.init(Cipher.DECRYPT_MODE, new MockKey());
            fail("Expected InvalidKeyException");
        } catch (InvalidKeyException expected) {
        } finally {
            Security.removeProvider(mockProvider.getName());
        }
    }

    /**
     * If a provider rejects a key for "Cipher/Mode/Padding"", there might be another that
     * accepts the key for "Cipher". Don't throw InvalidKeyException when trying the first one.
     * http://b/22208820
     */
    public void testCipher_init_tryAllCombinationsBeforeThrowingInvalidKey()
            throws Exception {
        Provider mockProvider = new MockProvider("MockProvider") {
            public void setup() {
                put("Cipher.FOO/FOO/FOO", MockCipherSpi.AllKeyTypes.class.getName());
                put("Cipher.FOO/FOO/FOO SupportedKeyClasses", "none");
            }
        };

        Provider mockProvider2 = new MockProvider("MockProvider2") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.AllKeyTypes.class.getName());
            }
        };

        Security.addProvider(mockProvider);

        try {
            try {
                // The provider installed doesn't accept the key.
                Cipher c = Cipher.getInstance("FOO/FOO/FOO");
                c.init(Cipher.DECRYPT_MODE, new MockKey());
                fail("Expected InvalidKeyException");
            } catch (InvalidKeyException expected) {
            }

            Security.addProvider(mockProvider2);

            try {
                // The new provider accepts "FOO" with this key. Use it despite the other provider
                // accepts "FOO/FOO/FOO" but doesn't accept the key.
                Cipher c = Cipher.getInstance("FOO/FOO/FOO");
                c.init(Cipher.DECRYPT_MODE, new MockKey());
                assertEquals("MockProvider2", c.getProvider().getName());
            } finally {
                Security.removeProvider(mockProvider2.getName());
            }
        } finally {
            Security.removeProvider(mockProvider.getName());
        }
    }

    /**
     * http://b/29038928
     * If in a second call to init the current spi doesn't support the new specified key, look for
     * another suitable spi.
     */
    public void test_init_onKeyTypeChange_reInitCipher() throws Exception {
        Provider mockProvider = new MockProvider("MockProvider") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.SpecificKeyTypes.class.getName());
            }
        };
        Provider mockProvider2 = new MockProvider("MockProvider2") {
            public void setup() {
                put("Cipher.FOO", MockCipherSpi.SpecificKeyTypes2.class.getName());
            }
        };
        try {
            Security.addProvider(mockProvider);
            Security.addProvider(mockProvider2);
            Cipher cipher = Cipher.getInstance("FOO");
            cipher.init(Cipher.ENCRYPT_MODE, new MockKey());
            assertEquals("MockProvider", cipher.getProvider().getName());
            // Using a different key...
            cipher.init(Cipher.ENCRYPT_MODE, new MockKey2());
            // ...results in a different provider.
            assertEquals("MockProvider2", cipher.getProvider().getName());
        } finally {
            Security.removeProvider(mockProvider.getName());
            Security.removeProvider(mockProvider2.getName());
        }
    }

    /**
     * http://b/29038928
     * If in a second call to init the current spi doesn't support the new specified
     * {@link AlgorithmParameterSpec}, look for another suitable spi.
     */
    public void test_init_onAlgorithmParameterTypeChange_reInitCipher() throws Exception {
        Provider mockProvider = new MockProvider("MockProvider") {
            public void setup() {
                put("Cipher.FOO",
                        MockCipherSpi.SpecificAlgorithmParameterSpecTypes.class.getName());
            }
        };
        Provider mockProvider2 = new MockProvider("MockProvider2") {
            public void setup() {
                put("Cipher.FOO",
                        MockCipherSpi.SpecificAlgorithmParameterSpecTypes2.class.getName());
            }
        };
        try {
            Security.addProvider(mockProvider);
            Security.addProvider(mockProvider2);
            Cipher cipher = Cipher.getInstance("FOO");
            cipher.init(Cipher.ENCRYPT_MODE,
                    new MockKey(),
                    new MockCipherSpi.MockAlgorithmParameterSpec());
            assertEquals("MockProvider", cipher.getProvider().getName());
            // Using a different AlgorithmParameterSpec...
            cipher.init(Cipher.ENCRYPT_MODE,
                    new MockKey(),
                    new MockCipherSpi.MockAlgorithmParameterSpec2());
            // ...results in a different provider.
            assertEquals("MockProvider2", cipher.getProvider().getName());
        } finally {
            Security.removeProvider(mockProvider.getName());
            Security.removeProvider(mockProvider2.getName());
        }
    }

    /**
     * http://b/29038928
     * If in a second call to init the current spi doesn't support the new specified
     * {@link AlgorithmParameters}, look for another suitable spi.
     */
    public void test_init_onAlgorithmParametersChange_reInitCipher() throws Exception {
        Provider mockProvider = new MockProvider("MockProvider") {
            public void setup() {
                put("Cipher.FOO",
                        MockCipherSpi.SpecificAlgorithmParameterAesAlgorithm.class.getName());
            }
        };
        Provider mockProvider2 = new MockProvider("MockProvider2") {
            public void setup() {
                put("Cipher.FOO",
                        MockCipherSpi.SpecificAlgorithmParametersDesAlgorithm.class.getName());
            }
        };
        try {
            Security.addProvider(mockProvider);
            Security.addProvider(mockProvider2);
            Cipher cipher = Cipher.getInstance("FOO");
            cipher.init(Cipher.ENCRYPT_MODE,
                    new MockKey(),
                    AlgorithmParameters.getInstance("AES"));
            assertEquals("MockProvider", cipher.getProvider().getName());
            // Using a different AlgorithmParameters...
            cipher.init(Cipher.ENCRYPT_MODE,
                    new MockKey(),
                    AlgorithmParameters.getInstance("DES"));
            // ...results in a different provider.
            assertEquals("MockProvider2", cipher.getProvider().getName());
        } finally {
            Security.removeProvider(mockProvider.getName());
            Security.removeProvider(mockProvider2.getName());
        }
    }
}
