/* * eID Applet Project. * Copyright (C) 2008-2010 FedICT. * Copyright (C) 2014-2015 e-Contract.be BVBA. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version * 3.0 as published by the Free Software Foundation. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, see * http://www.gnu.org/licenses/. */ package be.fedict.eid.applet; import java.applet.AppletContext; import java.awt.CardLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.FlowLayout; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLEncoder; import java.security.AccessController; import java.security.Permission; import java.security.PrivilegedAction; import java.util.Locale; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JApplet; import javax.swing.JButton; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JProgressBar; import javax.swing.JRootPane; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.SwingUtilities; import javax.swing.UIManager; import be.fedict.eid.applet.Messages.MESSAGE_ID; /** * The main class of the eID Applet. The {@link #init()} method is where it all * starts. * * @author Frank Cornelis * @see Applet#init() */ public class Applet extends JApplet { private static final long serialVersionUID = 1L; public static final String TARGET_PAGE_PARAM = "TargetPage"; public static final String CANCEL_PAGE_PARAM = "CancelPage"; public static final String AUTHORIZATION_ERROR_PAGE_PARAM = "AuthorizationErrorPage"; public static final String BACKGROUND_COLOR_PARAM = "BackgroundColor"; public static final String FOREGROUND_COLOR_PARAM = "ForegroundColor"; public static final String LANGUAGE_PARAM = "Language"; public static final String MESSAGE_CALLBACK_PARAM = "MessageCallback"; public static final String MESSAGE_CALLBACK_EX_PARAM = "MessageCallbackEx"; public static final String HIDE_DETAILS_BUTTON_PARAM = "HideDetailsButton"; private JStatusLabel statusLabel; private JTextArea detailMessages; private JProgressBar progressBar; private boolean securityConditionTrustedWebApp; private void setStatusMessage(final Status status, Messages.MESSAGE_ID messageId) { final String statusMessage = this.messages.getMessage(messageId); try { SwingUtilities.invokeAndWait(new Runnable() { public void run() { Applet.this.statusLabel.setText(statusMessage); /* * this helps screen readers, simply using setText() does * not seem to work, at least not with Win2003 + JAB 2 + * JSE6u20 + JAWS 10 */ Applet.this.statusLabel.getAccessibleContext().setAccessibleName(statusMessage); if (Status.ERROR == status) { Applet.this.statusLabel.setForeground(Color.RED); Applet.this.progressBar.setIndeterminate(false); } Applet.this.statusLabel.invalidate(); if (false == Applet.this.hideDetailsButtonParam) { Applet.this.detailMessages.append(statusMessage + "\n"); Applet.this.detailMessages .setCaretPosition(Applet.this.detailMessages.getDocument().getLength()); } else { System.out.println(statusMessage); } } }); Applet.this.invokeMessageCallback(status, messageId); } catch (Exception e) { // tja } } protected void invokeMessageCallback(Status status, Messages.MESSAGE_ID messageId) { if (null == this.messageCallbackParam && null == this.messageCallbackExParam) { return; } ClassLoader classLoader = Applet.class.getClassLoader(); Class<?> jsObjectClass; try { jsObjectClass = classLoader.loadClass("netscape.javascript.JSObject"); } catch (ClassNotFoundException e) { String msg = "JSObject class not found"; if (false == this.hideDetailsButtonParam) { this.detailMessages.append(msg + "\n"); this.detailMessages.setCaretPosition(Applet.this.detailMessages.getDocument().getLength()); } else { System.out.println(msg); } return; } try { Method getWindowMethod = jsObjectClass.getMethod("getWindow", new Class<?>[] { java.applet.Applet.class }); Object jsObject = getWindowMethod.invoke(null, this); Method callMethod = jsObjectClass.getMethod("call", new Class<?>[] { String.class, Class.forName("[Ljava.lang.Object;") }); if (null != this.messageCallbackParam) { addDetailMessage("invoking Javascript message callback: " + this.messageCallbackParam); String statusMessage = this.messages.getMessage(messageId); callMethod.invoke(jsObject, this.messageCallbackParam, new Object[] { status.name(), statusMessage }); } if (null != this.messageCallbackExParam) { addDetailMessage("invoking Javascript message callback (ex): " + this.messageCallbackExParam); String statusMessage = this.messages.getMessage(messageId); callMethod.invoke(jsObject, this.messageCallbackExParam, new Object[] { status.name(), messageId.name(), statusMessage }); } } catch (Exception e) { String msg = "error locating: JSObject.getWindow().call: " + e.getMessage(); if (false == this.hideDetailsButtonParam) { this.detailMessages.append(msg + "\n"); this.detailMessages.setCaretPosition(Applet.this.detailMessages.getDocument().getLength()); } else { System.out.println(msg); } return; } } @Override public void init() { try { javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() { initUI(); } }); } catch (Exception e) { System.err.println("initUI didn't successfully complete: " + e.getMessage()); StackTraceElement[] stackTrace = e.getStackTrace(); for (StackTraceElement stackTraceElement : stackTrace) { System.err.println(stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName() + ":" + stackTraceElement.getLineNumber()); } } } private Thread workerThread; @Override public void start() { if (null == this.workerThread) { /* * We start only once. Restart doesn't make sense for eID * operations. */ this.workerThread = new Thread(new AppletThread()); this.workerThread.start(); } else { addDetailMessage("Restart detected."); } } private void gotoTargetPage() { String targetPageParam = getParameter(TARGET_PAGE_PARAM); if (null != targetPageParam) { AppletContext appletContext = getAppletContext(); URL documentBase = getDocumentBase(); try { URL targetUrl = new URL(documentBase, targetPageParam); addDetailMessage("Navigating to: " + targetUrl); appletContext.showDocument(targetUrl, "_self"); } catch (MalformedURLException e) { addDetailMessage("URL error: " + e.getMessage()); } } else { // Java WebStart System.exit(0); } } private boolean gotoCancelPage() { String cancelPageParam = getParameter(CANCEL_PAGE_PARAM); if (null == cancelPageParam) { return false; } AppletContext appletContext = getAppletContext(); URL documentBase = getDocumentBase(); try { URL targetUrl = new URL(documentBase, cancelPageParam); addDetailMessage("Navigating to: " + targetUrl); appletContext.showDocument(targetUrl, "_self"); } catch (MalformedURLException e) { addDetailMessage("URL error: " + e.getMessage()); } return true; } private void gotoAuthorizationErrorPage() { String authorizationErrorPage = getParameter(AUTHORIZATION_ERROR_PAGE_PARAM); if (null == authorizationErrorPage) { return; } AppletContext appletContext = getAppletContext(); URL documentBase = getDocumentBase(); try { URL targetUrl = new URL(documentBase, authorizationErrorPage); addDetailMessage("Navigating to: " + targetUrl); appletContext.showDocument(targetUrl, "_self"); } catch (MalformedURLException e) { addDetailMessage("URL error: " + e.getMessage()); } } private void addDetailMessage(final String detailMessage) { try { SwingUtilities.invokeAndWait(new Runnable() { public void run() { if (false == Applet.this.hideDetailsButtonParam) { Applet.this.detailMessages.append(detailMessage + "\n"); Applet.this.detailMessages .setCaretPosition(Applet.this.detailMessages.getDocument().getLength()); } else { System.out.println(detailMessage); } } }); } catch (Exception e) { // tja } } @Override public AppletContext getAppletContext() { if (false == this.securityConditionTrustedWebApp) { throw new SecurityException("web application not trusted"); } return super.getAppletContext(); } @Override public String getParameter(String name) { if (false == this.securityConditionTrustedWebApp) { throw new SecurityException("web application not trusted"); } return super.getParameter(name); } private void setBackgroundColor(Container container, Color backgroundColor) { for (Component component : container.getComponents()) { component.setBackground(backgroundColor); if (component instanceof Container) { setBackgroundColor((Container) component, backgroundColor); } } container.setBackground(backgroundColor); } private Messages messages; private String messageCallbackParam; private String messageCallbackExParam; private boolean hideDetailsButtonParam; private void initUI() { loadMessages(); initStyle(); this.messageCallbackParam = super.getParameter(MESSAGE_CALLBACK_PARAM); this.messageCallbackExParam = super.getParameter(MESSAGE_CALLBACK_EX_PARAM); String hideDetailsButtonParam = super.getParameter(HIDE_DETAILS_BUTTON_PARAM); if (null != hideDetailsButtonParam) { this.hideDetailsButtonParam = Boolean.parseBoolean(hideDetailsButtonParam); } else { this.hideDetailsButtonParam = false; } Container contentPane = this.getContentPane(); contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.PAGE_AXIS)); initStatusPanel(contentPane); contentPane.add(Box.createVerticalStrut(10)); initProgressBar(contentPane); if (false == this.hideDetailsButtonParam) { contentPane.add(Box.createVerticalStrut(10)); initDetailPanel(contentPane); } setupColors(contentPane); } private void initStyle() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { // tja } } private void loadMessages() { /* * super.getParameter to get around the security check. */ String languageParam = super.getParameter(LANGUAGE_PARAM); Locale locale; if (null != languageParam) { locale = new Locale(languageParam); } else { locale = this.getLocale(); } /* for screen readers */ JRootPane.setDefaultLocale(locale); this.messages = new Messages(locale); } private void initDetailPanel(Container container) { CardLayout cardLayout = new CardLayout(); JPanel detailPanel = new JPanel(cardLayout); initDetailButton(detailPanel, cardLayout); initDetailMessages(detailPanel); container.add(detailPanel); } private void initDetailButton(final Container container, final CardLayout cardLayout) { JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); String msg = this.messages.getMessage(MESSAGE_ID.DETAILS_BUTTON); JButton detailButton = new JButton(msg + " >>"); detailButton.getAccessibleContext().setAccessibleName(msg); detailButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent actionEvent) { cardLayout.next(container); } }); panel.add(detailButton); container.add(panel, "button"); } private void initDetailMessages(Container container) { this.detailMessages = new JTextArea(10, 80); this.detailMessages.setEditable(false); /* Detailed messages are only available in English */ this.detailMessages.setLocale(Locale.ENGLISH); this.detailMessages.getAccessibleContext().setAccessibleDescription("Detailed log messages"); JPopupMenu popupMenu = new JPopupMenu(); JMenuItem copyMenuItem = new JMenuItem(this.messages.getMessage(MESSAGE_ID.COPY_ALL)); copyMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Toolkit toolkit = Toolkit.getDefaultToolkit(); Clipboard clipboard = toolkit.getSystemClipboard(); StringSelection stringSelection = new StringSelection(Applet.this.detailMessages.getText()); clipboard.setContents(stringSelection, null); } }); popupMenu.add(copyMenuItem); addMailMenuItem(popupMenu); this.detailMessages.setComponentPopupMenu(popupMenu); JScrollPane scrollPane = new JScrollPane(this.detailMessages, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); container.add(scrollPane, "details"); } private void initProgressBar(Container container) { this.progressBar = new JProgressBar(); this.progressBar.setIndeterminate(true); container.add(this.progressBar); } private void initStatusPanel(Container container) { JPanel statusPanel = new JPanel(); statusPanel.setLayout(new BoxLayout(statusPanel, BoxLayout.LINE_AXIS)); String msg = this.messages.getMessage(MESSAGE_ID.LOADING); this.statusLabel = new JStatusLabel(msg); this.statusLabel.getAccessibleContext().setAccessibleName(msg); statusPanel.add(this.statusLabel); statusPanel.add(Box.createHorizontalGlue()); container.add(statusPanel); } private void setupColors(Container container) { String backgroundColorParam = super.getParameter(BACKGROUND_COLOR_PARAM); Color backgroundColor; if (null != backgroundColorParam) { backgroundColor = Color.decode(backgroundColorParam); } else { backgroundColor = Color.WHITE; } setBackgroundColor(container, backgroundColor); String foregroundColorParam = super.getParameter(FOREGROUND_COLOR_PARAM); if (null != foregroundColorParam) { Color foregroundColor = Color.decode(foregroundColorParam); this.statusLabel.setForeground(foregroundColor); if (false == this.hideDetailsButtonParam) { this.detailMessages.setForeground(foregroundColor); } } } private void addMailMenuItem(JPopupMenu popupMenu) { Thread currentThread = Thread.currentThread(); ClassLoader classLoader = currentThread.getContextClassLoader(); Class<?> desktopClass; try { desktopClass = classLoader.loadClass("java.awt.Desktop"); } catch (ClassNotFoundException e) { /* * In this case the user cannot email to the FedICT service desk. */ return; } try { Method isDesktopSupportedMethod = desktopClass.getMethod("isDesktopSupported"); Boolean desktopSupported = (Boolean) isDesktopSupportedMethod.invoke(null); if (false == desktopSupported) { return; } Method getDesktopMethod = desktopClass.getMethod("getDesktop"); final Object desktop = getDesktopMethod.invoke(null); final Method mailMethod = desktopClass.getMethod("mail", URI.class); JMenuItem emailMenuItem = new JMenuItem(this.messages.getMessage(MESSAGE_ID.MAIL)); emailMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { String message = Applet.this.detailMessages.getText(); try { URI mailUri = new URI( "mailto:" + URLEncoder.encode("eid-applet@googlegroups.com", "UTF-8") + "?subject=" + URLEncoder.encode("eID Applet Feedback", "UTF-8").replaceAll("\\+", "%20") + "&body=" + URLEncoder.encode(message, "UTF-8").replaceAll("\\+", "%20")); mailMethod.invoke(desktop, mailUri); } catch (Exception mailException) { Applet.this.addDetailMessage("error mailing message: " + mailException.getMessage()); } } }); popupMenu.add(emailMenuItem); } catch (Exception e) { return; } } private class AppletThread implements Runnable { public void run() { addDetailMessage("eID Applet - Copyright (C) 2008-2013 FedICT."); addDetailMessage("Copyright (C) 2014-2016 e-Contract.be BVBA."); addDetailMessage("Released under GNU LGPL version 3.0 license."); addDetailMessage("More info: https://github.com/e-Contract/eid-applet"); /* * first check required applet permissions. prevents that * cardTerminal.connect will trigger the security exception later on */ addDetailMessage("checking applet privileges..."); SecurityManager securityManager = System.getSecurityManager(); if (null == securityManager) { setStatusMessage(Status.ERROR, MESSAGE_ID.SECURITY_ERROR); addDetailMessage("no security manager found. not running as an applet?"); return; } Object securityContext = securityManager.getSecurityContext(); /* * Next trick is to remove the dependency on the Java 6 runtime. */ String javaVersion = System.getProperty("java.version"); if (javaVersion.startsWith("1.5")) { /* * TODO: also check some 1.5 PKCS#11 required permission * * Browsing the source code of OpenJDK you can see clearly that * the Sun PKCS#11 wrapper is/was the one from IAIK. Funny that * companies paid IAIK licenses while all that time Sun gave it * away for free. Business seems to be all about tricking stupid * people into spending money. */ } else { /* * Java 1.6 and later. */ addDetailMessage("security manager permission check for java 1.6..."); Permission permission; try { Class<?> cardPermissionClass = Class.forName("javax.smartcardio.CardPermission"); Constructor<?> cardPermissionConstructor = cardPermissionClass.getConstructor(String.class, String.class); permission = (Permission) cardPermissionConstructor.newInstance("*", "*"); } catch (Exception e) { setStatusMessage(Status.ERROR, MESSAGE_ID.GENERIC_ERROR); addDetailMessage("javax.smartcardio not available: " + e.getMessage()); return; } try { securityManager.checkPermission(permission, securityContext); } catch (SecurityException e) { setStatusMessage(Status.ERROR, MESSAGE_ID.SECURITY_ERROR); addDetailMessage("applet not authorized to access smart card. applet not signed?"); return; } /* * The Fedora IcedTea JRE browser plugin never gets the * permissions right, even if the applet JAR has been signed. */ } /* * Next check whether the user trusts the web application. */ addDetailMessage("checking web application trust..."); URL documentBase = getDocumentBase(); if (false == "https".equals(documentBase.getProtocol())) { if (false == "localhost".equals(documentBase.getHost())) { setStatusMessage(Status.ERROR, MESSAGE_ID.SECURITY_ERROR); addDetailMessage("web application not trusted."); addDetailMessage("use the web application via \"https\" instead of \"http\""); return; } else { addDetailMessage("trusting localhost web applications"); } } URL codeBase = getCodeBase(); if (false == "https".equals(codeBase.getProtocol())) { /* * for reasons of performance a web designer might choose to * keep the applet under http. * * Notice that in this case Firefox 3 also gives a warning that * the page contains partially unencrypted content. Of course we * know that the integrity of the applet JAR is preserved anyhow * because of the JAR digital signature. */ addDetailMessage("warning: web application (applet resource) not trusted."); } /* * Mark the web application as trusted */ Applet.this.securityConditionTrustedWebApp = true; /* * Next is to make sure we run privileged. */ AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { addDetailMessage("running privileged code..."); Controller controller = new Controller(new AppletView(), new AppletRuntime(), Applet.this.messages); controller.run(); return null; } }); } } private class AppletRuntime implements Runtime { @Override public URL getDocumentBase() { return Applet.this.getDocumentBase(); } @Override public String getParameter(String name) { return Applet.this.getParameter(name); } @Override public void gotoTargetPage() { Applet.this.gotoTargetPage(); } @Override public Applet getApplet() { return Applet.this; } @Override public boolean gotoCancelPage() { return Applet.this.gotoCancelPage(); } @Override public void gotoAuthorizationErrorPage() { Applet.this.gotoAuthorizationErrorPage(); } } private class AppletView implements View { public void addDetailMessage(String detailMessage) { Applet.this.addDetailMessage(detailMessage); } public Component getParentComponent() { return Applet.this.getParentComponent(); } public boolean privacyQuestion(boolean includeAddress, boolean includePhoto, String identityDataUsage) { return Applet.this.privacyQuestion(includeAddress, includePhoto, identityDataUsage); } public void setStatusMessage(Status status, Messages.MESSAGE_ID messageId) { Applet.this.setStatusMessage(status, messageId); } public void setProgressIndeterminate() { Applet.this.setProgressIndetermintate(); } public void resetProgress(int max) { Applet.this.resetProgress(max); } public void increaseProgress() { Applet.this.increaseProgress(); } @Override public void confirmAuthenticationSignature(String message) { Applet.this.confirmAuthenticationSignature(message); } @Override public int confirmSigning(String description, String digestAlgo) { return Applet.this.confirmSigning(description, digestAlgo); } } private boolean privacyQuestion(boolean includeAddress, boolean includePhoto, String identityDataUsage) { String msg = this.messages.getMessage(MESSAGE_ID.PRIVACY_QUESTION) + "\n" + this.messages.getMessage(MESSAGE_ID.IDENTITY_INFO) + ": " + this.messages.getMessage(MESSAGE_ID.IDENTITY_IDENTITY); if (includeAddress) { msg += ", " + this.messages.getMessage(MESSAGE_ID.IDENTITY_ADDRESS); } if (includePhoto) { msg += ", " + this.messages.getMessage(MESSAGE_ID.IDENTITY_PHOTO); } if (null != identityDataUsage) { msg += "\n" + this.messages.getMessage(MESSAGE_ID.USAGE) + ": " + identityDataUsage; } int response = JOptionPane.showConfirmDialog(this, msg, "Privacy", JOptionPane.YES_NO_OPTION); return response == JOptionPane.YES_OPTION; } private int confirmSigning(String description, String digestAlgo) { String signatureCreationLabel = this.messages.getMessage(MESSAGE_ID.SIGNATURE_CREATION); String signQuestionLabel = this.messages.getMessage(MESSAGE_ID.SIGN_QUESTION); String signatureAlgoLabel = this.messages.getMessage(MESSAGE_ID.SIGNATURE_ALGO); int response = JOptionPane .showConfirmDialog( this.getParentComponent(), signQuestionLabel + " \"" + description + "\"?\n" + signatureAlgoLabel + ": " + digestAlgo + " with RSA", signatureCreationLabel, JOptionPane.YES_NO_OPTION); return response; } private void confirmAuthenticationSignature(String message) { int response = JOptionPane.showConfirmDialog(this.getParentComponent(), message, "eID Authentication Signature", JOptionPane.YES_NO_OPTION); if (response != JOptionPane.YES_OPTION) { throw new SecurityException("user cancelled"); } } private int progress; private void resetProgress(int max) { this.progressBar.setMinimum(0); this.progressBar.setMaximum(max); this.progressBar.setIndeterminate(false); this.progressBar.setValue(0); this.progress = 0; } private void setProgressIndetermintate() { this.progressBar.setIndeterminate(true); } private void increaseProgress() { this.progress++; this.progressBar.setValue(this.progress); } private Component getParentComponent() { return this; } }