/*
 * Copyright (C) 2010 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 com.android.server;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.content.Context;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.File;
import java.nio.file.Files;
import java.util.Arrays;

/**
 * Tests for {@link com.android.server.EntropyMixer}
 */
@RunWith(AndroidJUnit4.class)
public class EntropyMixerTest {

    private static final int SEED_FILE_SIZE = EntropyMixer.SEED_FILE_SIZE;

    private Context context;
    private File seedFile;
    private File randomReadDevice;
    private File randomWriteDevice;

    @Before
    public void setUp() throws Exception {
        context = InstrumentationRegistry.getTargetContext();
        seedFile = createTempFile("entropy.dat");
        randomReadDevice = createTempFile("urandomRead");
        randomWriteDevice = createTempFile("urandomWrite");
    }

    private File createTempFile(String prefix) throws Exception {
        File file = File.createTempFile(prefix, null);
        file.deleteOnExit();
        return file;
    }

    private byte[] repeatByte(byte b, int length) {
        byte[] data = new byte[length];
        Arrays.fill(data, b);
        return data;
    }

    // Test initializing the EntropyMixer when the seed file doesn't exist yet.
    @Test
    public void testInitFirstBoot() throws Exception {
        seedFile.delete();

        byte[] urandomInjectedData = repeatByte((byte) 0x01, SEED_FILE_SIZE);
        Files.write(randomReadDevice.toPath(), urandomInjectedData);

        // The constructor should have the side effect of writing to
        // randomWriteDevice and creating seedFile.
        new EntropyMixer(context, seedFile, randomReadDevice, randomWriteDevice);

        // Since there was no old seed file, the data that was written to
        // randomWriteDevice should contain only device-specific information.
        assertTrue(isDeviceSpecificInfo(Files.readAllBytes(randomWriteDevice.toPath())));

        // The seed file should have been created.
        validateSeedFile(seedFile, new byte[0], urandomInjectedData);
    }

    // Test initializing the EntropyMixer when the seed file already exists.
    @Test
    public void testInitNonFirstBoot() throws Exception {
        byte[] previousSeed = repeatByte((byte) 0x01, SEED_FILE_SIZE);
        Files.write(seedFile.toPath(), previousSeed);

        byte[] urandomInjectedData = repeatByte((byte) 0x02, SEED_FILE_SIZE);
        Files.write(randomReadDevice.toPath(), urandomInjectedData);

        // The constructor should have the side effect of writing to
        // randomWriteDevice and updating seedFile.
        new EntropyMixer(context, seedFile, randomReadDevice, randomWriteDevice);

        // The data that was written to randomWriteDevice should consist of the
        // previous seed followed by the device-specific information.
        byte[] dataWrittenToUrandom = Files.readAllBytes(randomWriteDevice.toPath());
        byte[] firstPartWritten = Arrays.copyOf(dataWrittenToUrandom, SEED_FILE_SIZE);
        byte[] secondPartWritten =
                Arrays.copyOfRange(
                        dataWrittenToUrandom, SEED_FILE_SIZE, dataWrittenToUrandom.length);
        assertArrayEquals(previousSeed, firstPartWritten);
        assertTrue(isDeviceSpecificInfo(secondPartWritten));

        // The seed file should have been updated.
        validateSeedFile(seedFile, previousSeed, urandomInjectedData);
    }

    private boolean isDeviceSpecificInfo(byte[] data) {
        return new String(data).startsWith(EntropyMixer.DEVICE_SPECIFIC_INFO_HEADER);
    }

    private void validateSeedFile(File seedFile, byte[] previousSeed, byte[] urandomInjectedData)
            throws Exception {
        final int unhashedLen = SEED_FILE_SIZE - 32;
        byte[] newSeed = Files.readAllBytes(seedFile.toPath());
        assertEquals(SEED_FILE_SIZE, newSeed.length);
        assertEquals(SEED_FILE_SIZE, urandomInjectedData.length);
        assertFalse(Arrays.equals(newSeed, previousSeed));
        // The new seed should consist of the first SEED_FILE_SIZE - 32 bytes
        // that were read from urandom, followed by a 32-byte hash that should
        // *not* be the same as the last 32 bytes that were read from urandom.
        byte[] firstPart = Arrays.copyOf(newSeed, unhashedLen);
        byte[] secondPart = Arrays.copyOfRange(newSeed, unhashedLen, SEED_FILE_SIZE);
        byte[] firstPartInjected = Arrays.copyOf(urandomInjectedData, unhashedLen);
        byte[] secondPartInjected =
                Arrays.copyOfRange(urandomInjectedData, unhashedLen, SEED_FILE_SIZE);
        assertArrayEquals(firstPart, firstPartInjected);
        assertFalse(Arrays.equals(secondPart, secondPartInjected));
    }
}
