/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.netbeans.modules.keyring.win32;

import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;
import com.sun.jna.WString;
import com.sun.jna.win32.StdCallLibrary;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.modules.keyring.utils.Utils;
import org.netbeans.modules.keyring.spi.EncryptionProvider;
import org.openide.util.Utilities;
import org.openide.util.lookup.ServiceProvider;

/**
 * Data protection utility for Microsoft Windows.
 * XXX org.tmatesoft.svn.core.internal.util.jna.SVNWinCrypt is a possibly more robust implementation
 * (though it seems to set CRYPTPROTECT_UI_FORBIDDEN which we do not necessarily want).
 */
@ServiceProvider(service=EncryptionProvider.class, position=100)
public class Win32Protect implements EncryptionProvider {

    private static final Logger LOG = Logger.getLogger(Win32Protect.class.getName());

    public @Override boolean enabled() {
        if (!Utilities.isWindows()) {
            LOG.fine("not running on Windows");
            return false;
        }
        if (Boolean.getBoolean("netbeans.keyring.no.native")) {
            LOG.fine("native keyring integration disabled");
            return false;
        }
        try {
            if (CryptLib.INSTANCE == null) {
                LOG.fine("loadLibrary -> null");
                return false;
            }
            return true;
        } catch (Throwable t) {
            LOG.log(Level.FINE, null, t);
            return false;
        }
    }

    public @Override String id() {
        return "win32"; // NOI18N
    }

    public @Override byte[] encrypt(char[] cleartext) throws Exception {
        byte[] cleartextB = Utils.chars2Bytes(cleartext);
        CryptIntegerBlob input = new CryptIntegerBlob();
        input.store(cleartextB);
        Arrays.fill(cleartextB, (byte) 0);
        CryptIntegerBlob output = new CryptIntegerBlob();
        if (!CryptLib.INSTANCE.CryptProtectData(input, null, null, null, null, 0, output)) {
            throw new Exception("CryptProtectData failed: " + Native.getLastError());
        }
        input.zero();
        return output.load();
    }

    public @Override char[] decrypt(byte[] ciphertext) throws Exception {
        CryptIntegerBlob input = new CryptIntegerBlob();
        input.store(ciphertext);
        CryptIntegerBlob output = new CryptIntegerBlob();
        if (!CryptLib.INSTANCE.CryptUnprotectData(input, null, null, null, null, 0, output)) {
            throw new Exception("CryptUnprotectData failed: " + Native.getLastError());
        }
        byte[] result = output.load();
        // XXX gives CCE because not a Memory: output.zero();
        char[] cleartext = Utils.bytes2Chars(result);
        Arrays.fill(result, (byte) 0);
        return cleartext;
    }

    public @Override boolean decryptionFailed() {
        return false; // not much to do about it
    }

    public @Override void encryptionChangingCallback(Callable<Void> callback) {}

    public @Override void encryptionChanged() {
        assert false;
    }

    public @Override void freshKeyring(boolean fresh) {}

    public interface CryptLib extends StdCallLibrary {
        CryptLib INSTANCE = Native.load("Crypt32", CryptLib.class); // NOI18N
        /** @see <a href="http://msdn.microsoft.com/en-us/library/aa380261(VS.85,printer).aspx">Reference</a> */
        boolean CryptProtectData(
                CryptIntegerBlob pDataIn,
                WString szDataDescr,
                CryptIntegerBlob pOptionalEntropy,
                Pointer pvReserved,
                Pointer pPromptStruct,
                int dwFlags,
                CryptIntegerBlob pDataOut
        )/* throws LastErrorException*/;
        /** @see <a href="http://msdn.microsoft.com/en-us/library/aa380882(VS.85,printer).aspx">Reference</a> */
        boolean CryptUnprotectData(
                CryptIntegerBlob pDataIn,
                WString[] ppszDataDescr,
                CryptIntegerBlob pOptionalEntropy,
                Pointer pvReserved,
                Pointer pPromptStruct,
                int dwFlags,
                CryptIntegerBlob pDataOut
        )/* throws LastErrorException*/;
    }

    @SuppressWarnings("PublicField")
    @FieldOrder({"cbData", "pbData"})
    public static class CryptIntegerBlob extends Structure {
        public int cbData;
        public /*byte[]*/Pointer pbData;
        byte[] load() {
            return pbData.getByteArray(0, cbData);
            // XXX how to free pbData? [Kernel32]LocalFree?
        }
        void store(byte[] data) {
            cbData = data.length;
            pbData = new Memory(data.length);
            pbData.write(0, data, 0, cbData);
        }
        void zero() {
            ((Memory) pbData).clear();
        }
    }

}
