public class JNIMouseHook {

    private final JNIMouseHook.User32 USER32INST;
    private final Kernel32 KERNEL32INST;
    private final Component SOURCE;

    /**
     * @param eventSource - component
     */
    public JNIMouseHook(Component eventSource) {
        if (!Platform.isWindows()) {
            throw new UnsupportedOperationException("Not supported on this platform.");
        }
        SOURCE = eventSource;
        USER32INST = JNIMouseHook.User32.INSTANCE;

        KERNEL32INST = Kernel32.INSTANCE;
        mouseHook = hookTheMouse();
        Native.setProtected(true);

    }

    private static LowLevelMouseProc mouseHook;
    private WinUser.HHOOK hhk;
    private Thread thrd;
    private boolean threadFinish = true;
    private boolean isHooked = false;
    private static final int WM_MOUSEMOVE = 512;
    private static final int WM_LBUTTONDOWN = 513;
    private static final int WM_RBUTTONDOWN = 516;
    private static final int WM_MBUTTONDOWN = 519;

    /**
     * Stop watching...
     */
    public void unsetMouseHook() {
        threadFinish = true;
        if (thrd.isAlive()) {
            thrd.interrupt();
            thrd = null;
        }
        isHooked = false;
    }

    public boolean isIsHooked() {
        return isHooked;
    }

    /**
     * Start watching...
     */
    public void setMouseHook() {
        thrd = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    if (!isHooked) {
                        hhk = USER32INST.SetWindowsHookEx(14, mouseHook, KERNEL32INST.GetModuleHandle(null), 0);
                        isHooked = true;
                        WinUser.MSG msg = new WinUser.MSG();
                        while ((USER32INST.GetMessage(msg, null, 0, 0)) != 0) {
                            USER32INST.TranslateMessage(msg);
                            USER32INST.DispatchMessage(msg);
                            //System.out.print(isHooked);
                            if (!isHooked) {
                                break;
                            }
                        }
                    } else {
                    }
                } catch (Exception e) {
                    System.err.println(e.getMessage());
                    System.err.println("Caught exception in MouseHook!");
                }
            }
        }, "Named thread");
        threadFinish = false;
        thrd.start();

    }

    private interface LowLevelMouseProc extends WinUser.HOOKPROC {

        WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, MSLLHOOKSTRUCT lParam);
    }

    private LowLevelMouseProc hookTheMouse() {
        return new LowLevelMouseProc() {
            @Override
            public WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, MSLLHOOKSTRUCT info) {
                if (nCode >= 0) {

                    switch (wParam.intValue()) {
                        case JNIMouseHook.WM_LBUTTONDOWN:
                            addEventToQueue(info.pt.x, info.pt.y);
                            break;
                        case JNIMouseHook.WM_RBUTTONDOWN:
                            addEventToQueue(info.pt.x, info.pt.y);
                            break;
                        case JNIMouseHook.WM_MBUTTONDOWN:
                            addEventToQueue(info.pt.x, info.pt.y);
                            break;
                        case JNIMouseHook.WM_MOUSEMOVE:

                            break;
                        default:
                            break;
                    }
                    // unhook if needed
                    if (threadFinish == true) {
                        USER32INST.PostQuitMessage(0);
                    }
                }

                Pointer ptr = info.getPointer();
                long peer = Pointer.nativeValue(ptr);
                return USER32INST.CallNextHookEx(hhk, nCode, wParam, new LPARAM(peer));
            }
        };
    }

    private synchronized void addEventToQueue(int x, int y) {
        Point p = new Point(x, y);
        // from absolute to relative position
        p.setLocation(x - SOURCE.getLocationOnScreen().x, y - SOURCE.getLocationOnScreen().y);

        if (!SOURCE.contains(p)) {
            MouseClickOutsideComponentEvent event = new MouseClickOutsideComponentEvent(SOURCE, MouseEvent.MOUSE_CLICKED, new Date().getTime(), 0, p.x, p.y, 1, true, MouseEvent.BUTTON1);
            SOURCE.getToolkit().getDefaultToolkit().getSystemEventQueue().postEvent(event);
        }
    }

    public static class MSLLHOOKSTRUCT extends Structure {

        @Override
        protected List getFieldOrder() {
            return Arrays.asList(new String[]{"pt", "hwnd", "wHitTestCode", "dwExtraInfo"});
        }

        public static class ByReference extends MSLLHOOKSTRUCT implements Structure.ByReference {
        }

        public WinDef.POINT pt;
        public WinDef.HWND hwnd;
        public int wHitTestCode;
        public BaseTSD.ULONG_PTR dwExtraInfo;
    }

    public interface User32 extends com.sun.jna.platform.win32.User32 {

        User32 INSTANCE = (User32) Native.loadLibrary("user32",
                User32.class,
                W32APIOptions.DEFAULT_OPTIONS);

        WinDef.LRESULT SendMessage(WinDef.HWND hWnd, int msg, WinDef.WPARAM num1, byte[] num2);
    }
} public class MouseClickOutsideComponentEvent extends MouseEvent implements Serializable {

    public MouseClickOutsideComponentEvent(Component source, int id, long when, int modifiers, int x, int y, int clickCount, boolean popupTrigger, int button) {
        super(source, id, when, modifiers, x, y, clickCount, popupTrigger, button);
    }

    public MouseClickOutsideComponentEvent(Component source, int id, long when, int modifiers, int x, int y, int clickCount, boolean popupTrigger) {
        super(source, id, when, modifiers, x, y, clickCount, popupTrigger);
    }

    public MouseClickOutsideComponentEvent(Component source, int id, long when, int modifiers, int x, int y, int xAbs, int yAbs, int clickCount, boolean popupTrigger, int button) {
        super(source, id, when, modifiers, x, y, xAbs, yAbs, clickCount, popupTrigger, button);
    }
} public abstract class DeviceService {

    protected HardwareWalletService hardwareWalletService;
    protected HardwareWalletClient client;
    protected String deviceKey;
    byte[] signedData;
    byte[] challengeData;
    protected ReadDeviceData asyncKeyData;
    protected ReadDeviceData asyncSignData;
    private Timer timer;
    protected String deviceLabel;
    private String exceptionKey;
    String passphrase;

    public DeviceService() {
    }

    public HardwareWalletService getHardwareWalletService() {
        return hardwareWalletService;
    }

    public HardwareWalletClient getClient() {
        return client;
    }

    /**
     * <p>
     * Downstream consumer applications should respond to hardware wallet
     * events</p>
     *
     * @param event The hardware wallet event indicating a state change
     */
    @Subscribe
    public void onHardwareWalletEvent(HardwareWalletEvent event) throws GetIdentitiesFailedException {
        Logger.getLogger(DeviceService.class.getName()).log(Level.INFO, "Received USB event: {0}", new Object[]{event.getEventType().name()});
        Logger.getLogger(DeviceService.class.getName()).log(Level.FINE, "Received USB event message: {0}", new Object[]{event.getMessage()});

        switch (event.getEventType()) {
            case SHOW_DEVICE_FAILED:
                TrayProcess.handleException(new DeviceFailedException());
                System.exit(0);
                break;

            case SHOW_DEVICE_DETACHED:
                resetCachedData();
                TrayProcess.sessionTimer.stop();
                break;

            case SHOW_DEVICE_READY:
                this.deviceLabel = ((Features) event.getMessage().get()).getLabel();
                break;

            case SHOW_PIN_ENTRY:
                // Device requires the current PIN to proceed

                PinMatrixRequest request = (PinMatrixRequest) event.getMessage().get();
                String pin;
                switch (request.getPinMatrixRequestType()) {
                    case CURRENT:

                        PinPad pinPad = new PinPad();
                        pinPad.setVisible(true);

                        ExecutorService executor = Executors.newSingleThreadExecutor();
                        Future<Object> future = executor.submit(pinPad.getPinData());

                        try {
                            pin = (String) future.get(AgentConstants.PIN_WAIT_TIMEOUT, TimeUnit.SECONDS);
                        } catch (InterruptedException | ExecutionException | TimeoutException ex) {
                            Logger.getLogger(DeviceService.class.getName()).log(Level.FINE, "Timeout when waiting for PIN.");
                            hardwareWalletService.requestCancel();
                            pinPad.setVisible(false);

                            if (timer != null && timer.isRunning()) {
                                TrayProcess.handleException(new DeviceTimeoutException()); // only when called from GUI
                            }
                            break;
                        }

                        if (AgentConstants.PIN_CANCELLED_MSG.equals(pin)) {
                            hardwareWalletService.requestCancel();
                            break;
                        }

                        hardwareWalletService.providePIN(pin);
                        pinPad.setVisible(false);

                        break;
                }
                break;
            case SHOW_PASSPHRASE_ENTRY:
                // Device requires the current passphrase to proceed

                PassphraseDialog passphraseDialog = new PassphraseDialog();
                passphraseDialog.setVisible(true);

                ExecutorService passExecutor = Executors.newSingleThreadExecutor();
                Future<Object> passFuture = passExecutor.submit(passphraseDialog.getPassphraseData());

                try {
                    passphrase = (String) passFuture.get(AgentConstants.PASSPHRASE_WAIT_TIMEOUT, TimeUnit.SECONDS);
                } catch (InterruptedException | ExecutionException | TimeoutException ex) {
                    Logger.getLogger(DeviceService.class.getName()).log(Level.FINE, "Timeout when waiting for passphrase.");
                    hardwareWalletService.requestCancel();
                    passphraseDialog.setVisible(false);

                    if (timer != null && timer.isRunning()) {
                        TrayProcess.handleException(new DeviceTimeoutException()); // only when called from GUI
                    }
                    break;
                }

                if (AgentConstants.PASSPHRASE_CANCELLED_MSG.equals(passphrase)) {
                    hardwareWalletService.requestCancel();
                    break;
                }

                hardwareWalletService.providePassphrase(passphrase);
                passphraseDialog.setVisible(false);

                break;
            case PUBLIC_KEY_FOR_IDENTITY:
                // Successful identity public key
                PublicKey pubKey = (PublicKey) event.getMessage().get();
                String openSSHkey;

                try {
                    byte[] rawPub = pubKey.getHdNodeType().get().getPublicKey().get();
                    String curveName = AgentUtils.readSetting(settings, AgentConstants.SETTINGS_KEY_CURVE_NAME, AgentConstants.CURVE_NAME_NISTP256);

                    if (rawPub[0] == 0x00) { // this is ed25519                        
                        if (!AgentConstants.CURVE_NAME_ED25519.equals(curveName)) {
                            throw new RuntimeException(LocalizedLogger.getLocalizedMessage("INVALID_KEY_MISMATCH", curveName, AgentConstants.CURVE_NAME_ED25519));
                        }
                        Logger.getLogger(DeviceService.class.getName()).log(Level.FINE, "Device returned public key curve: {0}", AgentConstants.CURVE_NAME_ED25519);

                        openSSHkey = IdentityUtils.serializeSSHKeyFromEd25519(rawPub);
                    } else { // this is nistp256
                        if (!AgentConstants.CURVE_NAME_NISTP256.equals(curveName)) {
                            throw new RuntimeException(LocalizedLogger.getLocalizedMessage("INVALID_KEY_MISMATCH", curveName, AgentConstants.CURVE_NAME_NISTP256)); // e.g. using old trezor fw to retrieve ed25519, but device is returning nistp256 as default                       
                        }
                        Logger.getLogger(DeviceService.class.getName()).log(Level.FINE, "Device returned public key curve: {0}", AgentConstants.CURVE_NAME_NISTP256);

                        // Retrieve public key from node (not xpub)
                        ECPublicKey publicKey = IdentityUtils.decodeNISTP256PublicKeyFromBytes(rawPub);

                        // Decompress key
                        String decompressedSSHKey = IdentityUtils.serializeSSHKeyFromNistp256(publicKey);
                        openSSHkey = IdentityUtils.printOpenSSHkeyNistp256(decompressedSSHKey, null);
                    }

                    // Convert key to openSSH format
                    Logger.getLogger(DeviceService.class.getName()).log(Level.FINE, "SSH public key: {0}", openSSHkey);

                    setDeviceKey(openSSHkey); // this is for swing timer - frame window to display pubkey scenario
                    getAsyncKeyData().setDeviceData(openSSHkey); // this is for Callable.call() - ssh server asks identities before sign

                    Logger.getLogger(DeviceService.class.getName()).log(Level.INFO, "Operation {0} executed successfully", "SSH2_AGENT_GET_IDENTITIES");
                } catch (NoSuchAlgorithmException | InvalidKeySpecException | RuntimeException e) {
                    TrayProcess.createError(LocalizedLogger.getLocalizedMessage("INVALID_KEY_OR_ALG", e.getLocalizedMessage()), true, e);
                    // TODO: send error to SSH server to cancel auth process

                }

                AgentUtils.restartSessionTimer();
                break;

            case SIGNED_IDENTITY:
                // Successful identity signature
                SignedIdentity signature = (SignedIdentity) event.getMessage().get();

                signedData = signature.getSignatureBytes().get();
                Logger.getLogger(DeviceService.class.getName()).log(Level.FINE, "Signature: {0}", Utils.HEX.encode(signedData));
                getAsyncSignData().setDeviceData(signedData);

                Logger.getLogger(DeviceService.class.getName()).log(Level.INFO, "Operation {0} executed successfully", "SSH2_AGENT_SIGN_REQUEST");

                AgentUtils.restartSessionTimer(); // this is probably redundant, since get pubkey operation preceeds
                break;

            case SHOW_OPERATION_FAILED:

                getAsyncSignData().setDeviceData(AgentConstants.SIGN_FAILED_BYTE);
                getAsyncKeyData().setDeviceData(AgentConstants.GET_IDENTITIES_FAILED_STRING);

                Failure failure = (Failure) event.getMessage().get();
                //String errMsg = failure.getMessage();

                switch (failure.getType()) {
                    case PIN_INVALID:
                        Logger.getLogger(DeviceService.class.getName()).log(Level.FINE, "PIN_INVALID");
                        exceptionKey = ExceptionHandler.getErrorKeyForException(new InvalidPinException());
                        TrayProcess.createWarning(LocalizedLogger.getLocalizedMessage(exceptionKey));
                        break;
                    case ACTION_CANCELLED:
                        Logger.getLogger(DeviceService.class.getName()).log(Level.FINE, "ACTION_CANCELLED");
                        getAsyncSignData().setDeviceData(AgentConstants.SIGN_CANCELLED_BYTE); // no need to raise error, since sign fail was caused by user pressing Cancel button
                        break;
                    case PIN_CANCELLED:
                        Logger.getLogger(DeviceService.class.getName()).log(Level.FINE, "PIN_CANCELLED");
                        break;
                    case NOT_INITIALIZED:
                        if (!AgentConstants.PASSPHRASE_CANCELLED_MSG.equals(passphrase)) { // do not raise error when passphrase was cancelled, we are interested in device not initialized state or unknown curve
                            TrayProcess.createError(LocalizedLogger.getLocalizedMessage("NOT_INITIALIZED"), false, null);
                        }
                        break;
                    default:
                    // Ignore
                }
                if (timer != null && timer.isRunning()) {
                    timer.stop(); // stop swing timer
                }

                break;
            default:
            // Ignore
        }
    }

    public String getDeviceKey() {
        return deviceKey;
    }

    public void setDeviceKey(String deviceKey) {
        this.deviceKey = deviceKey;
    }

    public Timer getTimer() {
        return this.timer;
    }

    public void setTimer(Timer timer) {
        this.timer = timer;
    }

    public ReadDeviceData checkoutAsyncKeyData() {
        getAsyncKeyData().setDeviceData(null);
        return getAsyncKeyData();
    }

    public ReadDeviceData checkoutAsyncSignData() {
        getAsyncSignData().setDeviceData(null);
        return getAsyncSignData();
    }

    private void resetCachedData() {
        checkoutAsyncKeyData();
        checkoutAsyncSignData();
        setDeviceKey(null);
    }

    /**
     * @return the deviceLabel
     */
    public String getDeviceLabel() {
        return deviceLabel;
    }

    /**
     * @return the asyncKeyData
     */
    public ReadDeviceData getAsyncKeyData() {
        return asyncKeyData;
    }

    /**
     * @return the asyncSignData
     */
    public ReadDeviceData getAsyncSignData() {
        return asyncSignData;
    }

} public final class KeepKeyService extends DeviceService {

    private final AbstractKeepKeyHardwareWallet wallet;

    public KeepKeyService() {
        wallet = HardwareWallets.newUsbInstance(
                KeepKeyV1HidHardwareWallet.class,
                Optional.<Integer>absent(),
                Optional.<Integer>absent(),
                Optional.<String>absent()
        );

        // Wrap the hardware wallet in a suitable client to simplify message API
        client = new KeepKeyHardwareWalletClient(wallet);

        deviceLabel = AgentConstants.KEEPKEY_LABEL; // set default name before real one is obtained from HW

        // Wrap the client in a service for high level API suitable for downstream applications
        hardwareWalletService = new HardwareWalletService(client);
        hardwareWalletService.start();
        HardwareWalletEvents.subscribe(this);

        asyncKeyData = new ReadDeviceData<String>();
        asyncSignData = new ReadDeviceData<byte[]>();

        Logger.getLogger(KeepKeyService.class.getName()).log(Level.INFO, "KeepKey Service Started");
    }

    public static KeepKeyService startKeepKeyService() {
        return new KeepKeyService();
    }

    public AbstractKeepKeyHardwareWallet getWallet() {
        return wallet;
    }
} public class SSHAgent implements WindowProc {

    private User32 libU = null;
    private Kernel32 libK = null;
    private HWND hWnd = null;
    private WinNT.HANDLE sharedFile;
    private Pointer sharedMemory;

    private boolean createdCorrectly = false;
    private boolean mainLoopStarted = false;

    private HANDLE mutex = null; // mutex ref, for installer

    public SSHAgent() throws Exception {
        initCoreClasses();
        if (checkIfNoPageantProcessIsRunning()) {
            createProcess();
        } else {
            TrayProcess.createErrorWindow(LocalizedLogger.getLocalizedMessage("PAGEANT_IS_RUNNING"));
        }
    }

    /*
     * Creates and registers Windows process
     */
    private HWND createWindowsProcess() {

        WString windowClass = new WString(APPNAME);
        HMODULE hInst = libK.GetModuleHandle("");
        WNDCLASSEX wndclass = new WNDCLASSEX();
        wndclass.hInstance = hInst;
        wndclass.lpfnWndProc = SSHAgent.this;
        wndclass.lpszClassName = windowClass;
        // register window class
        libU.RegisterClassEx(wndclass);
        getLastError();
        // create new window
        HWND winprocess = libU
                .CreateWindowEx(
                        User32.WS_OVERLAPPED,
                        windowClass,
                        APPNAME,
                        0, 0, 0, 0, 0,
                        null, // WM_DEVICECHANGE contradicts parent=WinUser.HWND_MESSAGE
                        null, hInst, null);

        mutex = libK.CreateMutex(null, false, MUTEX_NAME);
        return winprocess;
    }

    /*
     * Start listening to Windows messages 
     */
    public void startMainLoop() {
        mainLoopStarted = true;
        MSG msg = new MSG();
        while (libU.GetMessage(msg, null, 0, 0) == 1) {
            libU.TranslateMessage(msg);
            libU.DispatchMessage(msg);
        }
    }

    /*
     * This method is called by Windows to deliver message
     */
    @Override
    public LRESULT callback(HWND hwnd, int uMsg, WPARAM wParam, LPARAM lParam) {
        Logger.getLogger(SSHAgent.class.getName()).log(Level.FINE, "Windows requests operation: {0}", uMsg);

        switch (uMsg) {
            case MY_WM_COPYDATA: {
                return processMessage(hwnd, wParam, lParam);
            }
            case WinUser.WM_CREATE: {
                return new LRESULT(0);
            }
            case WinUser.WM_DESTROY: {
                libU.PostQuitMessage(0);
                return new LRESULT(0);
            }
            default:
                return libU.DefWindowProc(hwnd, uMsg, wParam, lParam);
        }
    }

    private int getLastError() {
        int rc = libK.GetLastError();
        if (rc != 0) {
            Logger.getLogger(SSHAgent.class.getName()).log(Level.SEVERE, "error {0}", rc);
        }
        return rc;
    }

    private LRESULT processMessage(HWND hwnd, WPARAM wParam, LPARAM lParam) {
        WinBase.SECURITY_ATTRIBUTES psa = null;
        String mapname = readFileNameFromInput(lParam);
        sharedFile
                = libK.CreateFileMapping(WinBase.INVALID_HANDLE_VALUE,
                        psa,
                        WinNT.PAGE_READWRITE,
                        0,
                        8192, // AGENT_MAX_MSGLEN
                        mapname);

        sharedMemory
                = Kernel32.INSTANCE.MapViewOfFile(sharedFile,
                        WinNT.SECTION_MAP_WRITE,
                        0, 0, 0);

        int ret = answerIfDevicePresent(sharedMemory);
        disconnectFromSharedMemory();

        return new LRESULT(ret);
    }

    /*
     *  Method answers to recieved message and writes response to shared memory
     *  1 = success, 0 = fail - Putty protocol defined
     */
    private int answerMessage(Pointer sharedMemory) {
        byte[] buff = new byte[5];
        sharedMemory.read(0, buff, 0, 5);
        AgentUtils.stopGUITimer(); // Cancel pubkey window request when SSH operations are in progress

        byte type = buff[4];
        switch (type) {
            case SSH2_AGENTC_REQUEST_IDENTITIES:
                processKeysRequest(sharedMemory);
                return 1;
            case SSH2_AGENTC_SIGN_REQUEST:
                processSignRequest(sharedMemory);
                return 1;
            default:
                writeAndLogFailure(sharedMemory, "Request for unsupported operation: " + type);
                return 0;

        }
    }

    private void processKeysRequest(final Pointer sharedMemory) {
        java.util.List<PublicKeyDTO> certs;
        try {
            certs = DeviceWrapper.getIdentitiesResponse(true);
            // TODO: If subsequent ssh sign request wont come, it means server doesnt know provided key, should we report? log? all? none?

            ByteBuffer ret = writeCertificatesToFrame(certs, SSH2_AGENT_IDENTITIES_ANSWER);
            sharedMemory.write(0, ret.array(), 0, ret.array().length);

        } catch (DeviceTimeoutException ex) {
            TrayProcess.handleException(ex);
        } catch (GetIdentitiesFailedException ex) {
            Logger.getLogger(SSHAgent.class.getName()).log(Level.SEVERE, "Operation {0} failed", "SSH2_AGENT_GET_IDENTITIES");
        }
    }

    private byte[] getDataFromRequest(Pointer sharedMemory, int offset) {
        byte[] length = new byte[4];
        sharedMemory.read(offset, length, 0, 4);
        ByteBuffer bb = ByteBuffer.wrap(length);
        int dataLength = bb.getInt();
        byte[] data = new byte[dataLength];
        sharedMemory.read(offset + 4, data, 0, dataLength);
        return data;

    }

    private String readFileNameFromInput(LPARAM lParam) {
        Class structPlatformClass = (Platform.is64Bit()) ? PuttyStruct64.class : PuttyStruct32.class;
        PuttyStruct ps = (PuttyStruct) PuttyStruct.newInstance(structPlatformClass, new Pointer(lParam.longValue()));
        ps.read();
        return new String(ps.lpData.getString(0).getBytes(Charset.forName("US-ASCII")));
    }

    public void disconnectFromSharedMemory() {
        if (sharedMemory != null) {
            libK.UnmapViewOfFile(sharedMemory);
        }
        if (sharedFile != null) {
            libK.CloseHandle(sharedFile);
        }
    }

    private int answerIfDevicePresent(Pointer sharedMemory) {
        if (AgentUtils.checkDeviceAvailable()) {
            return answerMessage(sharedMemory);
        } else {
            writeAndLogFailure(sharedMemory, "Device not available.");
            return 0;
        }
    }

    private void writeAndLogFailure(Pointer sharedMemory, String messageToLog) {
        Logger.getLogger(SSHAgent.class.getName()).log(Level.SEVERE, messageToLog);
        byte[] buff = new byte[5];
        buff[4] = SSH_AGENT_FAILURE;
        buff[1] = 1;
        sharedMemory.write(0, buff, 0, buff.length);
    }

    private void processSignRequest(Pointer sharedMemory) {
        byte[] keyInBytes = getDataFromRequest(sharedMemory, 5);
        byte[] challengeData = getDataFromRequest(sharedMemory, 5 + 4 + keyInBytes.length);
        byte[] signedDataRaw;
        byte[] signedData = null;
        byte[] userName = unframeUsernameFromChallengeBytes(challengeData);
        boolean isSignatureValid = false;
        String keyTypeProvided = unframeKeyTypeFromProvidedSSHKey(keyInBytes);

        Logger.getLogger(SSHAgent.class.getName()).log(Level.FINE, "Server sent challenge: {0}", Base64.toBase64String(challengeData));
        Logger.getLogger(SSHAgent.class.getName()).log(Level.FINE, "Effective username: {0}", new String(userName));
        Logger.getLogger(SSHAgent.class.getName()).log(Level.FINE, "Effective public key: {0}", Base64.toBase64String(keyInBytes));

        try {
            signedDataRaw = DeviceWrapper.signChallenge(challengeData, userName);
            if (signedDataRaw == null || signedDataRaw.length != 65) {
                throw new SignFailedException("HW sign response must have 65 bytes, length: " + signedDataRaw.length);
            }

            Logger.getLogger(SSHAgent.class.getName()).log(Level.FINE, "SSH server returned key type: {0}", keyTypeProvided);
            switch (keyTypeProvided) {
                case IdentityUtils.NISTP256_KEY_PREFIX:
                    try {
                        isSignatureValid = IdentityUtils.isValidSignature(IdentityUtils.unframeUncompressedNistpKeyFromSSHKey(keyInBytes),
                                challengeData, IdentityUtils.createDERSignResponse(signedDataRaw)); // double check that SSH server sent public key that can verify signature provided by HW
                    } catch (Throwable th) {
                        throw new SignFailedException("Error occured while validating signature.", th);
                    }

                    if (isSignatureValid) {
                        signedData = IdentityUtils.createSSHSignResponseFromNistpKey(signedDataRaw);
                    } else { // isSignatureValid==false
                        throw new SignFailedException("Signature was validated using provided public key with negative result.");
                    }

                    break;
                case IdentityUtils.ED25519_KEY_PREFIX:
                    // TODO: isSignatureValid missing, find secure java validator for ed25519
                    signedData = IdentityUtils.createSSHSignResponseFromEd25519Key(signedDataRaw);
                    break;
                default:
                    throw new SignFailedException("SSH server returned unknown key type: " + keyTypeProvided);
            }

            if (signedData != null) {
                sharedMemory.write(0, signedData, 0, signedData.length);
                TrayProcess.createInfo(LocalizedLogger.getLocalizedMessage("CERT_USE_SUCCESS", new String(userName), TrayProcess.deviceService.getDeviceLabel()));
            } else {
                TrayProcess.createWarning(LocalizedLogger.getLocalizedMessage("CERT_USED_ERROR"));
            }
        } catch (DeviceTimeoutException | SignFailedException ex) {
            TrayProcess.handleException(ex);
        } catch (ActionCancelledException ex) {
            Logger.getLogger(SSHAgent.class.getName()).log(Level.FINE, "Sign operation cancelled on HW.");
        }
    }

    private int getResponseLength(List<PublicKeyDTO> certs) {
        int length = 4 + 1 + 4; // total length (1x int) + result code (1x byte) + no. of keys (1x int)
        for (PublicKeyDTO i : certs) {
            length += 4;
            length += i.getbPublicKey().length;
            length += 4;
            length += i.getbComment().length;
        }
        return length;
    }

    private ByteBuffer writeCertificatesToFrame(List<PublicKeyDTO> certs, byte resultCode) {
        int responseLength = getResponseLength(certs);
        ByteBuffer ret = ByteBuffer.allocate(responseLength);
        ret.put(AgentUtils.frameArray(writeCertificatesToArray(certs, resultCode, responseLength - 4)));
        return ret;
    }

    private byte[] writeCertificatesToArray(List<PublicKeyDTO> certs, byte resultCode, int frameLength) {
        ByteBuffer ret = ByteBuffer.allocate(frameLength);

        ret.put(resultCode);
        if (!certs.isEmpty()) {
            ret.putInt(certs.size()); // number of keys (not byte size)
            for (PublicKeyDTO i : certs) {
                ret.put(AgentUtils.frameArray(i.getbPublicKey()));
                ret.put(AgentUtils.frameArray(i.getbComment()));
            }
        }
        return ret.array();
    }

    private void initCoreClasses() throws Exception {
        try {
            libU = User32.INSTANCE;
            libK = Kernel32.INSTANCE;
        } catch (java.lang.UnsatisfiedLinkError | java.lang.NoClassDefFoundError ex) {
            TrayProcess.handleException(ex);
            throw new Exception(ex.toString());
        }
    }

    private boolean checkIfNoPageantProcessIsRunning() {
        HWND isRunning = libU.FindWindow(APPNAME, APPNAME);
        return isRunning == null;
    }

    private void createProcess() {
        hWnd = createWindowsProcess();
        setCreatedCorrectly(true);
        Logger
                .getLogger(SSHAgent.class
                        .getName()).log(Level.INFO, AgentConstants.APP_PUBLIC_NAME + " " + AgentConstants.VERSION + " started successfully");
    }

    public boolean isCreatedCorrectly() {
        return createdCorrectly;
    }

    private synchronized void setCreatedCorrectly(boolean value) {
        createdCorrectly = value;
    }

    public boolean isMainLoopStarted() {
        return mainLoopStarted;
    }

    public void exitProcess() {
        Logger.getLogger(SSHAgent.class
                .getName()).log(Level.FINE, "Sending exit signal.");
        libU.DestroyWindow(hWnd);
        libK.CloseHandle(mutex); // just in case, mutex should be destroyed by now by process exit
        setCreatedCorrectly(false);

        //TrayProcess.deviceService.getWallet().disconnect();
        //TrayProcess.deviceService.getHardwareWalletService().stopAndWait();
        TrayProcess.deviceService.getClient().softDetach();
    }

    private byte[] unframeUsernameFromChallengeBytes(byte[] challengeData) {
        byte[] username;

        ByteBuffer bb = ByteBuffer.wrap(challengeData, 0, challengeData.length);
        int dataLength = bb.getInt(0); // = should be 32 bytes, random data generated on SSH server side      
        bb.position(4 + dataLength + 4 + 1); // forward buffer to position where username frame starts
        int userNameLength = bb.getInt(4 + dataLength + 1); // determine the length of username
        username = new byte[userNameLength];
        bb.get(username, 0, userNameLength);

        return username;
    }

    private String unframeKeyTypeFromProvidedSSHKey(byte[] sshKey) {
        byte[] keyTypeBytes;

        ByteBuffer bb = ByteBuffer.wrap(sshKey, 0, sshKey.length);
        int keyTypeLength = bb.getInt(0); // determine the length of keytype
        bb.position(4); // forward buffer to position where keyType frame starts

        keyTypeBytes = new byte[keyTypeLength];
        bb.get(keyTypeBytes, 0, keyTypeLength);

        return new String(keyTypeBytes);
    }
} public final class TrezorService extends DeviceService {

    private final AbstractTrezorHardwareWallet wallet;

    public TrezorService() {
        wallet = HardwareWallets.newUsbInstance(
                TrezorV1HidHardwareWallet.class,
                Optional.<Integer>absent(),
                Optional.<Integer>absent(),
                Optional.<String>absent()
        );

        // Wrap the hardware wallet in a suitable client to simplify message API
        client = new TrezorHardwareWalletClient(wallet);

        deviceLabel = AgentConstants.TREZOR_LABEL; // set default name before real one is obtained from HW

        // Wrap the client in a service for high level API suitable for downstream applications
        hardwareWalletService = new HardwareWalletService(client);
        hardwareWalletService.start();
        HardwareWalletEvents.subscribe(this);

        asyncKeyData = new ReadDeviceData<String>();
        asyncSignData = new ReadDeviceData<byte[]>();

        Logger.getLogger(TrezorService.class.getName()).log(Level.INFO, "Trezor Service Started");
    }

    public static TrezorService startTrezorService() {
        return new TrezorService();
    }

    public AbstractTrezorHardwareWallet getWallet() {
        return wallet;
    }
} public final class AgentConstants {

    /*
     * GUI Settings
     */
    public static final String APP_PUBLIC_NAME = "Trezor SSH Agent";

    public static final String ICON16_PATH = "/icon16.png";
    public static final String ICON24_PATH = "/icon24.png";
    public static final String ICON32_PATH = "/icon32.png";
    public static final String ICON48_PATH = "/icon48.png";
    public static final String ICON64_PATH = "/icon64.png";
    public static final String ICON72_PATH = "/icon72.png";
    public static final String ICON96_PATH = "/icon96.png";
    public static final String ICON128_PATH = "/icon128.png";

    public static final String ICON_DESCRIPTION = "tray icon"; //Windows tray

    // Device settings:
    public static final String CURVE_NAME_NISTP256 = "nist256p1";
    public static final String CURVE_NAME_ED25519 = "ed25519";
    public static final String TREZOR_LABEL = "Trezor";
    public static final String KEEPKEY_LABEL = "KeepKey";

    public static final String PIN_CANCELLED_MSG = "_PIN_CANCEL_"; // for pinpad
    public static final String PASSPHRASE_CANCELLED_MSG = "_PASSPHRASE_CANCEL_"; // for passphrase
    public static final byte[] SIGN_FAILED_BYTE = {(byte) 0x00}; // sign operation failed
    public static final byte[] SIGN_CANCELLED_BYTE = {(byte) 0x01}; // sign operation failed because user cancelled
    public static final String GET_IDENTITIES_FAILED_STRING = "_FAILED_"; // for getpub operation

    public static final int PIN_WAIT_TIMEOUT = 120; //sec
    public static final int PASSPHRASE_WAIT_TIMEOUT = 120; //sec
    public static final int KEY_WAIT_TIMEOUT = 120; //sec
    public static final int SIGN_WAIT_TIMEOUT = 120; //sec
    public static final int ASYNC_CHECK_INTERVAL = 10; //ms    

    /*
     * Settings file properties
     */
    public static final String SETTINGS_KEY_DEVICE = "DEVICE";
    public static final String SETTINGS_KEY_BIP32_URI = "BIP32_URI";
    public static final String SETTINGS_KEY_BIP32_INDEX = "BIP32_INDEX";
    public static final String SETTINGS_KEY_SESSION_TIMEOUT = "SESSION_TIMEOUT";
    public static final String SETTINGS_KEY_CURVE_NAME = "CURVE_NAME";
    public static final String SETTINGS_TREZOR_DEVICE = "trezor";
    public static final String SETTINGS_KEEPKEY_DEVICE = "keepkey";    
    public static final String SETTINGS_BIP32_SSHURI = "ssh://btc.rulez/connect";
    public static final String SETTINGS_BIP32_INDEX = "0";
    public static final String SETTINGS_SESSION_TIMEOUT = "60"; //mins

    /*
     * Logger and local settings
     */
    public static final String CONFIG_FILE_NAME = "logger.properties";

    public static final String DEFAULT_LOGGER_COUNTRY = "US";
    public static final String DEFAULT_LOGGER_LANGUAGE = "en";

    public static final String LOCALE_BUNDLES_PATH = "MessagesBundle";
    public static final String LOG_FILE_NAME = "Trezor_Agent.log"; // + change path in logger.properties
    public static final String SETTINGS_FILE_NAME = "Trezor_Agent.properties";

    public static String MUTEX_NAME = "Trezor_Agent_Mutex"; // mutex for installer - correlates with instaler.iss
    public static final String VERSION = "1.0.4-SNAPSHOT"; // global version, pom.xml

    /*
     * Error messages keys
     */
    public static final String DEVICE_TIMEOUT_KEY = "DEVICE_TIMEOUT";
    public static final String INCORRECT_PIN_ENTERED_KEY = "INCORRECT_PIN_ENTERED";
    public static final String ACTION_CANCELLED_KEY = "ACTION_CANCELLED";
    public static final String WRONG_URI_SYNTAX_KEY = "WRONG_URI_SYNTAX";
    public static final String UNKNOW_ERROR_KEY = "UNKNOW_ERROR";
    public static final String DEVICE_HW_FAILED_KEY = "DEVICE_HW_FAILED";
    public static final String NOT_SUPPORTED_ALGORITHM_KEY = "NOT_SUPPORTED_ALGORITHM";
    public static final String UNABLE_TO_USE_KEY_KEY = "UNABLE_TO_USE_KEY";
    public static final String SIGNATURE_EXCEPTION_KEY = "SIGNATURE_EXCEPTION";
    public static final String WALLET_NOT_PRESENT_KEY = "WALLET_NOT_PRESENT";
    public static final String DEVICE_NOT_READY_KEY = "DEVICE_NOT_READY";
    public static final String SIGN_FAILED_KEY = "SIGN_FAILED";
    public static final String GET_IDENTITIES_FAILED_KEY = "GET_IDENTITIES_FAILED";

    public static final String LINK_TO_LOG_KEY = "LINK_TO_LOG";
    public static final byte SSH2_AGENT_IDENTITIES_ANSWER = 12;
    public static final String APPNAME = "Pageant";
    public static final byte SSH2_AGENTC_SIGN_REQUEST = 13;
    /*
     * OpenSSH protocol SSH-2
     */
    public static final byte SSH2_AGENTC_REQUEST_IDENTITIES = 11;
    public static final byte SSH2_AGENT_SIGN_RESPONSE = 14;
    /*
     * SSH-1 and OpenSSH SSH-2 protocol commons
     */
    public static final int SSH_AGENT_FAILURE = 5;
    public static final int MY_WM_COPYDATA = 74;
} public class AgentUtils {


    public static byte[] frameArray(byte[] array) {
        ByteBuffer buffer = ByteBuffer.allocate(4 + array.length);
        buffer.putInt(array.length);
        buffer.put(array);
        return buffer.array();
    }

    public static byte[] frameArray(byte[] array1, byte[] array2) {
        return frameArray(ByteUtils.concatenate(array1, array2));
    }

    public static String getCurrentTimeStamp() {
        SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdfDate.format(new Date());
    }

    public static List<? extends Image> getAllIcons() {
        List<Image> icons = new ArrayList<>();
        icons.add(new ImageIcon(StartAgentGUI.class.getResource(AgentConstants.ICON16_PATH)).getImage());
        icons.add(new ImageIcon(StartAgentGUI.class.getResource(AgentConstants.ICON24_PATH)).getImage());
        icons.add(new ImageIcon(StartAgentGUI.class.getResource(AgentConstants.ICON32_PATH)).getImage());
        icons.add(new ImageIcon(StartAgentGUI.class.getResource(AgentConstants.ICON48_PATH)).getImage());
        icons.add(new ImageIcon(StartAgentGUI.class.getResource(AgentConstants.ICON64_PATH)).getImage());
        icons.add(new ImageIcon(StartAgentGUI.class.getResource(AgentConstants.ICON72_PATH)).getImage());
        icons.add(new ImageIcon(StartAgentGUI.class.getResource(AgentConstants.ICON96_PATH)).getImage());
        icons.add(new ImageIcon(StartAgentGUI.class.getResource(AgentConstants.ICON128_PATH)).getImage());
        return icons;
    }

    public static boolean checkDeviceAvailable() {
        if (TrayProcess.deviceService.getHardwareWalletService().isDeviceReady()) {
            if (TrayProcess.deviceService.getHardwareWalletService().isWalletPresent()) {
                return true;
            } else {
                TrayProcess.createWarning(LocalizedLogger.getLocalizedMessage("WALLET_NOT_PRESENT_KEY"));
            }
        } else {
            TrayProcess.createWarning(LocalizedLogger.getLocalizedMessage("DEVICE_NOT_READY_KEY", new Object[]{TrayProcess.deviceType}));
        }
        return false;
    }

    public static Properties initSettingsFile(File settings) throws IOException {
        // create file with default settings        

        Properties properties = new Properties();
        properties.setProperty(AgentConstants.SETTINGS_KEY_DEVICE, AgentConstants.SETTINGS_TREZOR_DEVICE);
        properties.setProperty(AgentConstants.SETTINGS_KEY_BIP32_URI, AgentConstants.SETTINGS_BIP32_SSHURI);
        properties.setProperty(AgentConstants.SETTINGS_KEY_BIP32_INDEX, AgentConstants.SETTINGS_BIP32_INDEX);
        properties.setProperty(AgentConstants.SETTINGS_KEY_SESSION_TIMEOUT, AgentConstants.SETTINGS_SESSION_TIMEOUT);
        properties.setProperty(AgentConstants.SETTINGS_KEY_CURVE_NAME, AgentConstants.CURVE_NAME_NISTP256);

        Logger.getLogger(AgentUtils.class.getName()).log(Level.FINE, "Setting default properties: {0}", new Object[]{properties});

        try (FileOutputStream fileOut = new FileOutputStream(settings)) {
            properties.store(fileOut, null);
        }

        return properties;
    }

    public static String readSetting(Properties settings, String key, String defaultValue) {
        String property = settings.getProperty(key);
        if (property == null) {
            Logger.getLogger(AgentUtils.class.getName()).log(Level.WARNING, "Settings property {0} not found, defaulting to value: {1}", new Object[]{key, defaultValue});
            return defaultValue;
        }
        Logger.getLogger(AgentUtils.class.getName()).log(Level.FINE, "Returning settings property {0}={1}", new Object[]{key, property});
        return property;
    }

    public static void stopGUITimer() {
        // GUI workaround, TODO: replace timers and do-whiles with proper async messaging
        Timer timer = TrayProcess.deviceService.getTimer();
        if (timer != null && timer.isRunning()) {
            Logger.getLogger(AgentUtils.class.getName()).log(Level.FINE, "Stopping GUI timer", new Object[]{});
            timer.stop(); // stop swing timer
        }
    }

    public static void restartSessionTimer() {
        if (TrayProcess.sessionTimer != null) {
            Logger.getLogger(DeviceWrapper.class.getName()).log(Level.FINE, "Restarting session timer.");
            TrayProcess.sessionTimer.restart(); // TODO: change some deviceService fields to static?
        } else {
            Logger.getLogger(DeviceWrapper.class.getName()).log(Level.SEVERE, "Session timer not initialized!");
        }
    }

    public static byte[] frameArrayWithUnsignedInt(byte[] array) {
        ByteBuffer buffer = ByteBuffer.allocate(1 + array.length); // unsigned int occupies only 1 byte
        buffer.put((byte) array.length);
        buffer.put(array);
        return buffer.array();
    }

    public static byte[] frameArrayWithUnsignedInt(byte[] array1, byte[] array2) {
        return AgentUtils.frameArrayWithUnsignedInt(ByteUtils.concatenate(array1, array2));
    }

} public class ExceptionHandler {

    public static String getErrorKeyForException(Throwable ex) {
        String key;

        try {
            throw ex;
        } catch (DeviceTimeoutException e) {
            key = DEVICE_TIMEOUT_KEY;
        } catch (InvalidPinException e) {
            key = INCORRECT_PIN_ENTERED_KEY;
        } catch (ActionCancelledException e) {
            key = ACTION_CANCELLED_KEY;
        } catch (URISyntaxException e) {
            key = WRONG_URI_SYNTAX_KEY;
        } catch (IOException e) {
            key = UNKNOW_ERROR_KEY;
        } catch (NoSuchAlgorithmException e) {
            key = NOT_SUPPORTED_ALGORITHM_KEY;
        } catch (InvalidKeyException | UnrecoverableKeyException e) {
            key = UNABLE_TO_USE_KEY_KEY;
        } catch (SignFailedException e) {
            key = SIGN_FAILED_KEY;
        } catch (GetIdentitiesFailedException e) {
            key = GET_IDENTITIES_FAILED_KEY;
        } catch (DeviceFailedException e) {
            key = DEVICE_HW_FAILED_KEY;
        } catch (Throwable e) {
            key = UNKNOW_ERROR_KEY;
        }

        return key;
    }
} public class LocalizedLogger {

    private static ResourceBundle messages;

    public static void setUpDefault() throws URISyntaxException, IOException {
        setupLoggerConfig();
        setLoggerLanguage(AgentConstants.DEFAULT_LOGGER_LANGUAGE, AgentConstants.DEFAULT_LOGGER_COUNTRY);
    }

    private static void setupLoggerConfig() throws URISyntaxException, IOException {
        LogManager.getLogManager().readConfiguration(LocalizedLogger.class.getResourceAsStream("/" + AgentConstants.CONFIG_FILE_NAME));
    }

    public static void setLoggerLanguage(String language, String country) {
        Locale currentLocale = new Locale(language, country);
        messages = ResourceBundle.getBundle(AgentConstants.LOCALE_BUNDLES_PATH, currentLocale);
    }

    public static String getLocalizedMessage(String key) {
        if (messages == null) {
            setLoggerLanguage(AgentConstants.DEFAULT_LOGGER_LANGUAGE, AgentConstants.DEFAULT_LOGGER_COUNTRY);
        }
        return messages.getString(key);

    }

    public static String getLocalizedMessage(String messageKey, Object... args) {
        if (messages == null) {
            setLoggerLanguage(AgentConstants.DEFAULT_LOGGER_LANGUAGE, AgentConstants.DEFAULT_LOGGER_COUNTRY);
        }
        return String.format(messages.getString(messageKey), args);
    }

}
import com.trezoragent.mouselistener.JNIMouseHook;
import com.trezoragent.mouselistener.MouseClickOutsideComponentEvent;
import com.trezoragent.sshagent.DeviceService;
import com.trezoragent.sshagent.KeepKeyService;
import com.trezoragent.sshagent.SSHAgent;
import com.trezoragent.sshagent.TrezorService;
import com.trezoragent.utils.AgentConstants;
import com.trezoragent.utils.AgentUtils;
import com.trezoragent.utils.ExceptionHandler;
import com.trezoragent.utils.LocalizedLogger;
import java.awt.AWTException;
import java.awt.Image;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
package com.trezoragent.gui;


/**
 *
 * @author martin.lizner
 */
public class TrayProcess {

    private static TrayIcon trayIcon;
    private static JNIMouseHook MOUSE_HOOK;
    private final static String VISIBLE_PROPERTY = "visible";

    public static SSHAgent agent;
    public static DeviceService deviceService;

    public static Properties settings;
    public static String deviceType;

    public static Timer sessionTimer;

    protected static void start() throws Exception {
        agent = new SSHAgent();

        if (agent.isCreatedCorrectly()) {

            File settingsFile = new File(System.getProperty("user.home") + File.separator + AgentConstants.SETTINGS_FILE_NAME);