public class PuttyStruct64 extends PuttyStruct {
    public long cbData;

    public PuttyStruct64(Pointer p) {
        super(p);
    }
} public class PuttyStruct32 extends PuttyStruct {
    public int cbData;

    public PuttyStruct32(Pointer p) {
        super(p);
    }
} public class PublicKeyDTO {
    private String sComment;
    private String sPublicKey;
    private byte[] bComment;
    private byte[] bPublicKey;

    public PublicKeyDTO() {
        this.bComment = null;
        this.bPublicKey = null;
        this.bComment = null;
        this.bPublicKey = null;
    }

    public PublicKeyDTO(byte[] bComment, byte[] bPublicKey) {
        this.bComment = bComment;
        this.bPublicKey = bPublicKey;
        this.bComment = null;
        this.bPublicKey = null;
    }

    public PublicKeyDTO(String sComment, String sPublicKey, byte[] bComment, byte[] bPublicKey, String owner) {
        this.sComment = sComment;
        this.sPublicKey = sPublicKey;
        this.bComment = bComment;
        this.bPublicKey = bPublicKey;
    }

    public String getsComment() {
        return sComment;
    }

    public void setsComment(String sComment) {
        this.sComment = sComment;
    }

    public String getsPublicKey() {
        return sPublicKey;
    }

    public void setsPublicKey(String sPublicKey) {
        this.sPublicKey = sPublicKey;
    }

    public byte[] getbComment() {
        return bComment;
    }

    public void setbComment(byte[] bComment) {
        this.bComment = bComment;
    }

    public byte[] getbPublicKey() {
        return bPublicKey;
    }

    public void setbPublicKey(byte[] bPublicKey) {
        this.bPublicKey = bPublicKey;
    }
     
} public abstract class PuttyStruct extends Structure {
    public int dwData;
    public Pointer lpData;    

    @Override
    protected List getFieldOrder() {
        return Arrays.asList(new String[]{"dwData", "cbData", "lpData"});
    }

    public PuttyStruct(Pointer p) {
        super(p);
    }

} public class ActionCancelledException extends Exception {

} public class DeviceTimeoutException extends Exception {

} public class GetIdentitiesFailedException extends Exception {

    public GetIdentitiesFailedException() {
        super();
    }

    public GetIdentitiesFailedException(Exception e) {
        super(e);
    }

} public class SignFailedException extends Exception {

    public SignFailedException(String s) {
        super(s);
    }

    public SignFailedException(String message, Throwable cause) {
        super(message, cause);
    }
} 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);

            if (!settingsFile.exists()) {
                try {
                    settings = AgentUtils.initSettingsFile(settingsFile); // create default settings file
                    Logger.getLogger(TrayProcess.class.getName()).log(Level.INFO, "New settings file created: {0}", new Object[]{settingsFile.getPath()});
                } catch (Exception ex) {
                    TrayProcess.createError(LocalizedLogger.getLocalizedMessage("INIT_SETTINGS_FILE_ERROR", ex.getLocalizedMessage()), false, ex);
                    //Logger.getLogger(TrayProcess.class.getName()).log(Level.SEVERE, null, ex);
                }
            } else {
                settings = new Properties();
                try (FileInputStream fileIn = new FileInputStream(settingsFile)) {
                    settings.load(fileIn);
                }
                Logger.getLogger(TrayProcess.class.getName()).log(Level.INFO, "Existing settings file loaded: {0}", new Object[]{settingsFile.getPath()});
            }

            // start device USB service depending on device type
            String deviceTypeProperty = AgentUtils.readSetting(settings, AgentConstants.SETTINGS_KEY_DEVICE, AgentConstants.TREZOR_LABEL);
            switch (deviceTypeProperty.toLowerCase()) {
                case (AgentConstants.SETTINGS_KEEPKEY_DEVICE):
                    deviceType = AgentConstants.KEEPKEY_LABEL;
                    deviceService = KeepKeyService.startKeepKeyService();
                    break;
                default:
                    deviceType = AgentConstants.TREZOR_LABEL;
                    deviceService = TrezorService.startTrezorService();
            }

            initSessionTimer(); // start timer to control session (PIN+Passphrase) expiration

            SwingUtilities.invokeLater(new Runnable() { // start GUI
                @Override
                public void run() {
                    TrayProcess.createAndShowGUI();
                }
            });

            agent.startMainLoop(); // start SSH Agent emulating Pageant and listening Windows requests
        }
    }

    private static void createAndShowGUI() {
        if (!SystemTray.isSupported()) {
            Logger.getLogger(StartAgentGUI.class.getName()).log(Level.SEVERE, "SYSTRAY_NOT_SUPPORTED");
            agent.exitProcess();
            return;
        }

        trayIcon = new TrayIcon(TrayProcess.createImage(AgentConstants.ICON16_PATH, AgentConstants.ICON_DESCRIPTION), AgentConstants.APP_PUBLIC_NAME);
        final SystemTray tray = SystemTray.getSystemTray();
        final AgentPopUpMenu popUpMenu = new AgentPopUpMenu(tray, trayIcon, agent, deviceService);

        MOUSE_HOOK = new JNIMouseHook(popUpMenu);
        trayIcon.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    popUpMenu.setLocation(e.getX(), e.getY());
                    popUpMenu.setInvoker(popUpMenu);
                    popUpMenu.setVisible(true);
                }
            }
        });

        popUpMenu.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e instanceof MouseClickOutsideComponentEvent) {
                    popUpMenu.setVisible(false);
                }
            }
        });

        popUpMenu.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if (evt.getPropertyName().equals(VISIBLE_PROPERTY)) {
                    if (evt.getNewValue().equals(Boolean.TRUE)) {
                        if (!MOUSE_HOOK.isIsHooked()) {
                            MOUSE_HOOK.setMouseHook();
                        }
                    } else if (MOUSE_HOOK.isIsHooked()) {
                        MOUSE_HOOK.unsetMouseHook();
                    }
                }
            }
        });

        try {
            tray.add(trayIcon);
        } catch (AWTException e) {
            Logger.getLogger(StartAgentGUI.class.getName()).log(Level.SEVERE, "TRAY_ICON_LOAD_ERROR", e);
            return;
        }
        trayIcon.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(null, LocalizedLogger.getLocalizedMessage("APPLICATION_INFO", AgentConstants.VERSION));
            }
        });
    }

    // Display exceptions to GUI
    public static void handleException(Throwable ex) {
        String exceptionKey = ExceptionHandler.getErrorKeyForException(ex);
        createError(LocalizedLogger.getLocalizedMessage(exceptionKey, ex), true, ex);
        //Logger.getLogger(SSHAgent.class.getName()).log(Level.SEVERE, "", ex);
    }

    public static void createWarning(String message) {
        if (trayIcon != null) {
            trayIcon.displayMessage(AgentConstants.APP_PUBLIC_NAME, message, TrayIcon.MessageType.WARNING);
        }
        Logger.getLogger(SSHAgent.class.getName()).log(Level.WARNING, message);
    }

    public static void createErrorWindow(String message) {
        JOptionPane.showMessageDialog(null, message, AgentConstants.APP_PUBLIC_NAME, JOptionPane.ERROR_MESSAGE);
        Logger.getLogger(SSHAgent.class.getName()).log(Level.SEVERE, message);
    }

    protected static Image createImage(String path, String description) {
        URL imageURL = StartAgentGUI.class.getResource(path);
        if (imageURL == null) {
            Logger.getLogger(StartAgentGUI.class.getName()).log(Level.SEVERE, "RESOURCE_NOT_FOUND", path);
            return null;
        } else {
            return (new ImageIcon(imageURL, description)).getImage();
        }
    }

    public static void createError(String message, boolean addLogLinkMessage, Throwable ex) {
        String message2 = message;
        if (addLogLinkMessage) {
            message2 = message.concat("\n").concat(LocalizedLogger.getLocalizedMessage(AgentConstants.LINK_TO_LOG_KEY));
        }
        if (trayIcon != null) {
            trayIcon.displayMessage(AgentConstants.APP_PUBLIC_NAME, message2, TrayIcon.MessageType.ERROR);
        }
        Logger.getLogger(SSHAgent.class.getName()).log(Level.SEVERE, message, ex);
    }

    public static void createInfo(String message) {
        if (trayIcon != null) {
            trayIcon.displayMessage(AgentConstants.APP_PUBLIC_NAME, message, TrayIcon.MessageType.INFO);
        }
        Logger.getLogger(SSHAgent.class.getName()).log(Level.INFO, message);
    }

    private static void initSessionTimer() {
        Integer delay = 1000 * 60 * new Integer(AgentUtils.readSetting(settings, AgentConstants.SETTINGS_KEY_SESSION_TIMEOUT, AgentConstants.SETTINGS_SESSION_TIMEOUT));
        sessionTimer = new Timer(delay, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent event) {
                TrayProcess.deviceService.getClient().clearSession();
                Logger.getLogger(TrayProcess.class.getName()).log(Level.INFO, "Clear session request has been sent to the device.");
            }
        }
        );
        sessionTimer.setRepeats(false);
    }

} 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 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 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);
    }

} public class IdentityUtils {

    public static final String NISTP256_KEY_PREFIX = "ecdsa-sha2-nistp256";
    public static final String NISTP256_CURVE_NAME = "nistp256";
    public static final String ED25519_KEY_PREFIX = "ssh-ed25519";
    static final byte[] ZERO = {(byte) 0};
    static final byte[] OCTET30 = {(byte) 48};
    static final byte[] SSH2_AGENT_SIGN_RESPONSE_ARRAY = {AgentConstants.SSH2_AGENT_SIGN_RESPONSE};
    static final byte[] OCTET02 = {(byte) 2};

    /**
     * <p>
     * Build an AddressN chain code structure for an Identity URI</p>
     *
     * <p>
     * A BIP-32 chain code is derived from a combination of the URI and the
     * index as follows:</p>
     * <ol>
     * <li>Concatenate the little endian representation of index with the URI
     * (index + URI)</li>
     * <li>Compute the SHA256 hash of the result (256 bits)</li>
     * <li>Take first 128 bits (16 bytes) of the hash and split it into four
     * 32-bit numbers A, B, C, D using little endian</li>
     * <li>Set highest bits of numbers A, B, C, D to 1 (e.g. bitwise-OR with
     * 0x80000000)</li>
     * <li>Derive the hardened HD node m/13'/A'/B'/C'/D' according to BIP32</li>
     * </ol>
     *
     * <p>
     * See https://github.com/satoshilabs/slips/blob/master/slip-0013.md for
     * more details</p>
     *
     * @param identityUri The identity URI (e.g.
     * "https://user@multibit.org/trezor-connect")
     * @param index The index of the identity to use (default is zero) to allow
     * for multiple identities on same path
     *
     * @return The list representing the chain code (only a simple chain is
     * currently supported)
     */
    public static List<Integer> buildAddressN(URI identityUri, int index) {

        // Convert index to little endian (Java is big endian by default)
        byte[] leIndex = LittleEndianConversions.I2OSP(index);

        // Convert URI to bytes
        byte[] identityUriBytes = identityUri.toASCIIString().getBytes(Charsets.UTF_8);

        // Concatenate index and URI
        byte[] canonicalBytes = ByteUtils.concatenate(leIndex, identityUriBytes);

        // SHA256(canonical)
        byte[] sha256CanonicalBytes = Sha256Hash.hash(canonicalBytes);

        // Truncate to first 128 bits (16 bytes) of SHA256
        byte[] truncatedSha256CanonicalBytes = ByteUtils.subArray(sha256CanonicalBytes, 0, 16);

        // Extract A,B,C,D in little endian form
        int[] abcdBytes = LittleEndianConversions.toIntArray(truncatedSha256CanonicalBytes);

        // Build m/13'/a'/b'/c'/d'
        return Lists.newArrayList(
                13 | ChildNumber.HARDENED_BIT,
                abcdBytes[0] | ChildNumber.HARDENED_BIT,
                abcdBytes[1] | ChildNumber.HARDENED_BIT,
                abcdBytes[2] | ChildNumber.HARDENED_BIT,
                abcdBytes[3] | ChildNumber.HARDENED_BIT
        );
    }

    /**
     * <p>
     * Get an EC public key from a byte array suitable for use with ssh
     * operations</p>
     *
     * <p>
     * Note: This is not a Bitcoin EC public key</p>
     *
     * @param pubKey The ecdsa-sha2-nistp256 EC public key encoded as bytes
     *
     * @return An EC public key
     *
     * @throws NoSuchAlgorithmException If ECDSA is not available
     * @throws InvalidKeySpecException If the key is invalid
     */
    public static ECPublicKey decodeNISTP256PublicKeyFromBytes(byte[] pubKey) throws NoSuchAlgorithmException, InvalidKeySpecException {

        ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("P-256");
        KeyFactory kf = KeyFactory.getInstance("ECDSA", new BouncyCastleProvider());
        ECNamedCurveSpec params = new ECNamedCurveSpec("P-256", spec.getCurve(), spec.getG(), spec.getN());
        ECPoint point = ECPointUtil.decodePoint(params.getCurve(), pubKey);
        ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params);
        ECPublicKey pk = (ECPublicKey) kf.generatePublic(pubKeySpec);

        return pk;
    }

    /**
     * <p>
     * Get an SSH key from the compressed EC public key in base64</p>
     *
     * @param publicKey The ecdsa-sha2-nistp256 EC public key
     *
     * @return An ssh key-only base64 format of public key from given EC public
     * key
     */
    public static String serializeSSHKeyFromNistp256(ECPublicKey publicKey) {

        ByteBuffer buffer = ByteBuffer.allocate(104);
        buffer.putInt(NISTP256_KEY_PREFIX.getBytes(Charsets.UTF_8).length);
        buffer.put(NISTP256_KEY_PREFIX.getBytes(Charsets.UTF_8));

        buffer.putInt(NISTP256_CURVE_NAME.getBytes(Charsets.UTF_8).length);
        buffer.put(NISTP256_CURVE_NAME.getBytes(Charsets.UTF_8));

        byte[] octet = {(byte) 0x04}; // this is special byte for SSH
        byte[] x = publicKey.getW().getAffineX().toByteArray(); // get X, Y cords of ECPoint
        byte[] y = publicKey.getW().getAffineY().toByteArray();
        byte[] x32 = ByteUtils.subArray(x, x.length - 32, x.length); //get last 32 bytes
        byte[] y32 = ByteUtils.subArray(y, y.length - 32, y.length);
        byte[] data = ByteUtils.concatenate(octet, ByteUtils.concatenate(x32, y32));

        buffer.putInt(data.length);
        buffer.put(data);

        return Base64.toBase64String(buffer.array());
    }

    public static String serializeSSHKeyFromEd25519(byte[] pubKey) {
        byte[] pubKeyWorking = ByteUtils.clone(pubKey);
        if (pubKeyWorking[0] == 0x00) {
            pubKeyWorking = ByteUtils.subArray(pubKeyWorking, 1); //strip the first byte
        }

        byte[] keyTypeFrame = AgentUtils.frameArray(ED25519_KEY_PREFIX.getBytes(Charsets.UTF_8));
        byte[] pubKeyFrame = AgentUtils.frameArray(pubKeyWorking);
        String serializedKey = Base64.toBase64String(ByteUtils.concatenate(keyTypeFrame, pubKeyFrame)); // easier than nistp256, we do not need to uncompress pubkey to x and y, we just send it compressed as device provided

        return ED25519_KEY_PREFIX + " " + serializedKey;
    }

    /**
     * <p>
     * Format a decompressed SSH key for use with OpenSSH library</p>
     *
     * @param base64Pubkey The ecdsa-sha2-nistp256 public key in base64 format
     * @param comment An optional comment (null will be ignored)
     *
     * @return prints ecdsa-sha2-nistp256 key in full ssh format with optional
     * comment
     */
    public static String printOpenSSHkeyNistp256(String base64Pubkey, String comment) {

        StringBuilder openSSH = new StringBuilder();
        openSSH.append(NISTP256_KEY_PREFIX);
        openSSH.append(" ");
        openSSH.append(base64Pubkey);
        if (comment != null) {
            openSSH.append(" ");
            openSSH.append(comment);
        }

        return openSSH.toString();
    }

    /**
     * <p>
     * Verify an SSH signature against a given public key and message</p>
     *
     * @param pubKey The ecdsa-sha2-nistp256 public key
     * @param message The message
     * @param signature The ASN.1 encoded ECDSA signature
     *
     * @return True if the signature is valid
     *
     * @throws NoSuchAlgorithmException If SHA256 with ECDSA is not available
     * @throws NoSuchProviderException If the BouncyCastle provider is not
     * available
     * @throws InvalidKeyException If the key is not valid
     * @throws SignatureException If the signature cannot be parsed
     * @throws InvalidKeySpecException If the key cannot be parsed
     */
    public static boolean isValidSignature(byte[] pubKey, byte[] message, byte[] signature)
            throws NoSuchAlgorithmException, NoSuchProviderException,
            InvalidKeyException, SignatureException, InvalidKeySpecException {

        Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", new BouncyCastleProvider());
        ecdsaVerify.initVerify(decodeNISTP256PublicKeyFromBytes(pubKey));
        ecdsaVerify.update(message);

        return ecdsaVerify.verify(signature);
    }

    public static byte[] createDERSignResponse(byte[] trezorSign) {
        byte[] xSign = ByteUtils.subArray(trezorSign, 0, 33); // signed integer
        byte[] ySign = ByteUtils.subArray(trezorSign, 33, trezorSign.length);
        byte[] xFrame = ByteUtils.concatenate(OCTET02, AgentUtils.frameArrayWithUnsignedInt(xSign)); // add special octet byte
        byte[] yFrame = ByteUtils.concatenate(OCTET02, AgentUtils.frameArrayWithUnsignedInt(ySign));
        byte[] xyFrame = AgentUtils.frameArrayWithUnsignedInt(xFrame, yFrame);
        byte[] sigBytes = ByteUtils.concatenate(OCTET30, xyFrame);
        return sigBytes;
    }

    public static byte[] createSSHSignResponseFromNistpKey(byte[] trezorSign) {
        byte[] noOctet = ByteUtils.subArray(trezorSign, 1, trezorSign.length); // remove first byte from 65byte array
        byte[] xSign = ByteUtils.subArray(noOctet, 0, 32); // divide 64byte array into halves
        byte[] ySign = ByteUtils.subArray(noOctet, 32, noOctet.length);
        xSign = ByteUtils.concatenate(ZERO, xSign); // add zero byte
        ySign = ByteUtils.concatenate(ZERO, ySign);
        byte[] sigBytes = ByteUtils.concatenate(AgentUtils.frameArray(xSign), AgentUtils.frameArray(ySign));
        byte[] dataArray = AgentUtils.frameArray(AgentUtils.frameArray(NISTP256_KEY_PREFIX.getBytes(Charsets.UTF_8)), AgentUtils.frameArray(sigBytes));
        return AgentUtils.frameArray(SSH2_AGENT_SIGN_RESPONSE_ARRAY, dataArray);
    }
    
    public static byte[] createSSHSignResponseFromEd25519Key(byte[] trezorSign) {
        byte[] noOctet = ByteUtils.subArray(trezorSign, 1, trezorSign.length); // remove first byte from 65byte array
        byte[] dataArray = AgentUtils.frameArray(AgentUtils.frameArray(ED25519_KEY_PREFIX.getBytes(Charsets.UTF_8)), AgentUtils.frameArray(noOctet));
        return AgentUtils.frameArray(SSH2_AGENT_SIGN_RESPONSE_ARRAY, dataArray);
    }

    /*
    - First byte in return value is encoding type, SSH use "4" to signalize uncompressed POINT. (uncompressed means both X and Y are provided)
    - Doc: http://grepcode.com/file/repo1.maven.org/maven2/com.madgag/scprov-jdk15on/1.47.0.1/org/spongycastle/math/ec/ECCurve.java
     */
    public static byte[] unframeUncompressedNistpKeyFromSSHKey(byte[] pubKeySSH) {
        byte[] compressedKey;
        ByteBuffer bb = ByteBuffer.wrap(pubKeySSH, 0, pubKeySSH.length);
        int data1Length = bb.getInt(0); // determine length of the 1st frame
        int data2Length = bb.getInt(4 + data1Length); // determine length of the 2nd frame
        //int data3Length = bb.getInt(4 + data1Length + data2Length + 4); // determine length of 3rd frame = 65bytes key we are looking for
        bb.position(4 + data1Length + data2Length + 4 + 4); // forward buffer to the 3rd frame
        compressedKey = new byte[65];
        bb.get(compressedKey, 0, 65); // read the 3rd uncompressed frame (1B octet + 32B X cord + 32B Y cord)
        return compressedKey;
    }

}

import com.trezoragent.struct.PuttyStruct64;
import com.trezoragent.struct.PuttyStruct32;
import com.trezoragent.struct.PublicKeyDTO;
import com.trezoragent.struct.PuttyStruct;
import com.sun.jna.Platform;
import com.sun.jna.Pointer;
import com.sun.jna.WString;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef.*;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.platform.win32.WinUser;
import com.sun.jna.platform.win32.WinUser.*;
import com.sun.jna.platform.win32.WinUser.WindowProc;
import com.trezoragent.exception.ActionCancelledException;
import com.trezoragent.exception.DeviceTimeoutException;
import com.trezoragent.exception.GetIdentitiesFailedException;
import com.trezoragent.exception.SignFailedException;
import com.trezoragent.gui.TrayProcess;
import com.trezoragent.utils.AgentConstants;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.logging.Level;
import static com.trezoragent.utils.AgentConstants.*;
import com.trezoragent.utils.AgentUtils;
import com.trezoragent.utils.LocalizedLogger;
import java.util.List;
import java.util.logging.Logger;
import com.trezoragent.utils.IdentityUtils;
import org.spongycastle.util.encoders.Base64;
        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;