/*******************************************************************************
 * Copyright (c) 2008, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.internal.security.win32;

import java.io.IOException;
import java.security.SecureRandom;

import javax.crypto.spec.PBEKeySpec;

import org.eclipse.equinox.internal.security.auth.AuthPlugin;
import org.eclipse.equinox.internal.security.auth.nls.SecAuthMessages;
import org.eclipse.equinox.internal.security.storage.Base64;
import org.eclipse.equinox.internal.security.win32.nls.WinCryptoMessages;
import org.eclipse.equinox.security.storage.ISecurePreferences;
import org.eclipse.equinox.security.storage.StorageException;
import org.eclipse.equinox.security.storage.provider.IPreferencesContainer;
import org.eclipse.equinox.security.storage.provider.PasswordProvider;

/**
 * Provides interface with native Windows data protection API. This provider
 * auto-generates separate passwords for each secure preferences tree.
 */
public class WinCrypto extends PasswordProvider {

	native public byte[] windecrypt(byte[] encryptedText);

	native public byte[] winencrypt(byte[] clearText);

	static {
		System.loadLibrary("jnicrypt64");
	}
	
	private final static String WIN_PROVIDER_NODE = "/org.eclipse.equinox.secure.storage/windows64";
	private final static String PASSWORD_KEY = "encryptedPassword";

	/**
	 * The length of the randomly generated password in bytes
	 */
	private final static int PASSWORD_LENGTH = 250;

	@Override
	public PBEKeySpec getPassword(IPreferencesContainer container, int passwordType) {
		byte[] encryptedPassword;
		if ((passwordType & CREATE_NEW_PASSWORD) == 0)
			encryptedPassword = getEncryptedPassword(container);
		else
			encryptedPassword = null;
		
		if (encryptedPassword != null) {
			byte[] decryptedPassword = windecrypt(encryptedPassword);
			if (decryptedPassword != null) {
				String password = new String(decryptedPassword);
				return new PBEKeySpec(password.toCharArray());
			} else {
				StorageException e = new StorageException(StorageException.ENCRYPTION_ERROR, WinCryptoMessages.decryptPasswordFailed);
				AuthPlugin.getDefault().logError(WinCryptoMessages.decryptPasswordFailed, e);
				return null;
			}
		}

		// add info message in the log
		AuthPlugin.getDefault().logMessage(WinCryptoMessages.newPasswordGenerated);
		
		byte[] rawPassword = new byte[PASSWORD_LENGTH];
		SecureRandom random = new SecureRandom();
		random.setSeed(System.currentTimeMillis());
		random.nextBytes(rawPassword);
		String password = Base64.encode(rawPassword);
		if (savePassword(password, container))
			return new PBEKeySpec(password.toCharArray());
		else
			return null;
	}

	private byte[] getEncryptedPassword(IPreferencesContainer container) {
		ISecurePreferences node = container.getPreferences().node(WIN_PROVIDER_NODE);
		String passwordHint; 
		try {
			passwordHint = node.get(PASSWORD_KEY, null);
		} catch (StorageException e) { // should never happen in this scenario
			AuthPlugin.getDefault().logError(WinCryptoMessages.decryptPasswordFailed, e);
			return null;
		}
		if (passwordHint == null)
			return null;
		return Base64.decode(passwordHint);
	}

	private boolean savePassword(String password, IPreferencesContainer container){
		byte[] data = winencrypt(password.getBytes());
		if (data == null) { // this is bad. Something wrong with OS or JNI.
			StorageException e = new StorageException(StorageException.ENCRYPTION_ERROR, WinCryptoMessages.encryptPasswordFailed);
			AuthPlugin.getDefault().logError(WinCryptoMessages.encryptPasswordFailed, e);
			return false;
		}
		String encodedEncryptyedPassword = Base64.encode(data);
		ISecurePreferences node = container.getPreferences().node(WIN_PROVIDER_NODE);
		try {
			node.put(PASSWORD_KEY, encodedEncryptyedPassword, false); // note we don't recursively try to encrypt
		} catch (StorageException e) { // should never happen in this scenario
			AuthPlugin.getDefault().logError(SecAuthMessages.errorOnSave, e);
			return false;
		}
		try {
			node.flush(); // save right away
		} catch (IOException e) {
			AuthPlugin.getDefault().logError(SecAuthMessages.errorOnSave, e);
			return false;
		}
		return true;
	}

	@Override
	public boolean retryOnError(Exception e, IPreferencesContainer container) {
		// It would be rather dangerous to allow this password to be changed
		// as it would permanently trash all entries in the secure storage.
		// Rather applications using get...() should handle exceptions and offer to overwrite 
		// data on an entry-by-entry scale.
		return false;
	}

}
