All Downloads are FREE. Search and download functionalities are using the official Maven repository.

apdu4j.SCTool Maven / Gradle / Ivy

/*
 * Copyright (c) 2014-2017 Martin Paljak
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package apdu4j;

import apdu4j.remote.*;
import jnasmartcardio.Smartcardio.EstablishContextException;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;

import javax.net.ssl.KeyManagerFactory;
import javax.smartcardio.*;
import javax.smartcardio.CardTerminals.State;
import java.awt.*;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.Provider;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;

public final class SCTool {
    private static final String CMD_LIST = "list";
    private static final String CMD_APDU = "apdu";
    private static final String OPT_SHELL = "shell";
    private static final String OPT_PROVIDER = "provider";

    private static final String OPT_READER = "reader";
    private static final String OPT_ALL = "all";
    private static final String OPT_VERBOSE = "verbose";
    private static final String OPT_DEBUG = "debug";
    private static final String OPT_VERSION = "version";
    private static final String OPT_ERROR = "error";
    private static final String OPT_DUMP = "dump";
    private static final String OPT_REPLAY = "replay";

    private static final String OPT_HELP = "help";
    private static final String OPT_SUN = "sun";
    private static final String OPT_T0 = "t0";
    private static final String OPT_T1 = "t1";
    private static final String OPT_EXCLUSIVE = "exclusive";
    private static final String OPT_CONNECT = "connect";
    private static final String OPT_PINNED = "pinned";
    private static final String OPT_P12 = "p12";
    private static final String OPT_WAIT = "wait";


    private static final String OPT_NO_GET_RESPONSE = "no-get-response";
    private static final String OPT_LIB = "lib";
    private static final String OPT_WEB = "web";
    private static final String OPT_PROVIDERS = "P";
    private static final String OPT_TEST_SERVER = "testserver";


    private static boolean verbose = false;

    private static OptionSet parseOptions(String[] argv) throws IOException {
        OptionSet args = null;
        OptionParser parser = new OptionParser();
        parser.acceptsAll(Arrays.asList("l", CMD_LIST), "list readers");
        parser.acceptsAll(Arrays.asList("p", OPT_PROVIDER), "specify provider").withRequiredArg();
        parser.acceptsAll(Arrays.asList("v", OPT_VERBOSE), "be verbose");
        parser.acceptsAll(Arrays.asList("d", OPT_DEBUG), "show debug");
        parser.acceptsAll(Arrays.asList("e", OPT_ERROR), "fail if not 0x9000");
        parser.acceptsAll(Arrays.asList("h", OPT_HELP), "show help");
        parser.acceptsAll(Arrays.asList("r", OPT_READER), "use reader").withRequiredArg();
        parser.acceptsAll(Arrays.asList("a", CMD_APDU), "send APDU").withRequiredArg();
        parser.acceptsAll(Arrays.asList("w", OPT_WEB), "open ATR in web");
        parser.acceptsAll(Arrays.asList("V", OPT_VERSION), "show version information");
        parser.acceptsAll(Arrays.asList("s", OPT_SHELL), "start shell");

        parser.accepts(OPT_DUMP, "save dump to file").withRequiredArg().ofType(File.class);
        parser.accepts(OPT_REPLAY, "replay command from dump").withRequiredArg().ofType(File.class);

        parser.accepts(OPT_SUN, "load SunPCSC");
        parser.accepts(OPT_CONNECT, "connect to URL or host:port").withRequiredArg();
        parser.accepts(OPT_P12, "path:pass of client PKCS#12").withRequiredArg();
        parser.accepts(OPT_PINNED, "require certificate").withRequiredArg().ofType(File.class);

        parser.accepts(OPT_PROVIDERS, "list providers");
        parser.accepts(OPT_ALL, "process all readers");
        parser.accepts(OPT_WAIT, "wait for card insertion");
        parser.accepts(OPT_T0, "use T=0");
        parser.accepts(OPT_T1, "use T=1");
        parser.accepts(OPT_EXCLUSIVE, "use EXCLUSIVE mode (JNA only)");
        parser.accepts(OPT_TEST_SERVER, "run a test server on port 10000").withRequiredArg();
        parser.accepts(OPT_NO_GET_RESPONSE, "don't use GET RESPONSE with SunPCSC");
        parser.accepts(OPT_LIB, "use specific PC/SC lib with SunPCSC").withRequiredArg();

        // Parse arguments
        try {
            args = parser.parse(argv);
            // Try to fetch all values so that format is checked before usage
            for (String s : parser.recognizedOptions().keySet()) {
                args.valuesOf(s);
            }
        } catch (OptionException e) {
            if (e.getCause() != null) {
                System.err.println(e.getMessage() + ": " + e.getCause().getMessage());
            } else {
                System.err.println(e.getMessage());
            }
            System.err.println();
            help_and_exit(parser, System.err);
        }
        if (args.has(OPT_HELP)) {
            help_and_exit(parser, System.out);
        }
        return args;
    }

    public static void main(String[] argv) throws Exception {
        OptionSet args = parseOptions(argv);

        if (args.has(OPT_VERBOSE)) {
            verbose = true;
            // Set up slf4j simple in a way that pleases us
            System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
            if (args.has(OPT_DEBUG)) {
                System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
            }
            System.setProperty("org.slf4j.simpleLogger.showThreadName", "true");
            System.setProperty("org.slf4j.simpleLogger.showShortLogName", "true");
            System.setProperty("org.slf4j.simpleLogger.levelInBrackets", "true");
        } else {
            System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "warn");
        }

        if (args.has(OPT_VERSION)) {
            String version = "apdu4j " + getVersion();
            // Append host information
            version += "\nRunning on " + System.getProperty("os.name");
            version += " " + System.getProperty("os.version");
            version += " " + System.getProperty("os.arch");
            version += ", Java " + System.getProperty("java.version");
            version += " by " + System.getProperty("java.vendor");
            System.out.println(version);
        }
        if (args.has(OPT_TEST_SERVER)) {
            // TODO: have the possibility to run SocketServer as well, based on argument (tcp: vs http:)
            RemoteTerminalServer srv = new RemoteTerminalServer(TestServer.class);
            srv.start(string2socket((String) args.valueOf(OPT_TEST_SERVER)));
            System.out.println("Hit ctrl-c to quit");
            while (true) {
                Thread.sleep(5000);
                srv.gc(System.currentTimeMillis() - 5 * 60 * 1000); // 5 minutes
            }
        }

        // We always bundle JNA with the tool, so add it to the providers.
        Security.addProvider(new jnasmartcardio.Smartcardio());

        // List TerminalFactory providers
        if (args.has(OPT_PROVIDERS)) {
            Provider providers[] = Security.getProviders("TerminalFactory.PC/SC");
            if (providers != null) {
                System.out.println("Existing TerminalFactory providers:");
                for (Provider p : providers) {
                    System.out.println(p.getName() + " v" + p.getVersion() + " (" + p.getInfo() + ")");
                }
            }
        }

        // Fix (SunPCSC) properties on non-windows platforms
        TerminalManager.fixPlatformPaths();

        // Only applies to SunPCSC
        if (args.has(OPT_NO_GET_RESPONSE)) {
            System.setProperty("sun.security.smartcardio.t0GetResponse", "false");
            System.setProperty("sun.security.smartcardio.t1GetResponse", "false");
        }

        // Override PC/SC library path (Only applies to SunPCSC)
        if (args.has(OPT_LIB)) {
            System.setProperty("sun.security.smartcardio.library", (String) args.valueOf(OPT_LIB));
        }

        final TerminalFactory tf;
        CardTerminals terminals = null;

        try {
            // Get a terminal factory
            if (args.has(OPT_PROVIDER)) {
                tf = TerminalManager.getTerminalFactory((String) args.valueOf(OPT_PROVIDER));
            } else if (args.has(OPT_SUN)) {
                tf = TerminalManager.getTerminalFactory(TerminalManager.SUN_CLASS);
            } else {
                tf = TerminalManager.getTerminalFactory(TerminalManager.JNA_CLASS);
            }

            if (verbose) {
                System.out.println("# Using " + tf.getProvider().getClass().getCanonicalName() + " - " + tf.getProvider());
                if (System.getProperty(TerminalManager.LIB_PROP) != null) {
                    System.out.println("# " + TerminalManager.LIB_PROP + "=" + System.getProperty(TerminalManager.LIB_PROP));
                }
            }
            // Get all terminals
            terminals = tf.terminals();
        } catch (EstablishContextException e) {
            String msg = TerminalManager.getExceptionMessage(e);
            fail("No readers: " + msg);
        }

        // Terminals to work on
        List do_readers = new ArrayList<>();

        try {
            // List Terminals
            if (args.has(CMD_LIST)) {
                List terms = terminals.list();
                if (verbose) {
                    System.out.println("# Found " + terms.size() + " terminal" + (terms.size() == 1 ? "" : "s"));
                }
                if (terms.size() == 0) {
                    fail("No readers found");
                }
                // List trminals
                for (CardTerminal t : terms) {
                    String vmd = " ";
                    if (verbose) {
                        try (PinPadTerminal pp = PinPadTerminal.getInstance(t)) {
                            pp.probe();
                            // Verify, Modify, Display
                            vmd += "[";
                            vmd += pp.canVerify() ? "V" : " ";
                            vmd += pp.canModify() ? "M" : " ";
                            vmd += pp.hasDisplay() ? "D" : " ";
                            vmd += "] ";
                        } catch (CardException e) {
                            String err = TerminalManager.getExceptionMessage(e);
                            if (err.equals(SCard.SCARD_E_SHARING_VIOLATION)) {
                                vmd = " [   ] ";
                            } else
                                vmd = " [EEE] ";
                        }
                    }
                    String present = t.isCardPresent() ? "[*]" : "[ ]";
                    String secondline = null;
                    String thirdline = null;

                    if (args.has(OPT_VERBOSE) && t.isCardPresent()) {
                        Card c = null;
                        byte[] atr = null;
                        // Try shared mode, to detect exclusive mode via exception
                        try {
                            c = t.connect("*");
                            atr = c.getATR().getBytes();
                        } catch (CardException e) {
                            String err = TerminalManager.getExceptionMessage(e);
                            // Detect exclusive mode. Hopes this always succeeds
                            if (err.equals(SCard.SCARD_E_SHARING_VIOLATION)) {
                                present = "[X]";
                                try {
                                    c = t.connect("DIRECT");
                                    atr = c.getATR().getBytes();
                                } catch (CardException e2) {
                                    String err2 = TerminalManager.getExceptionMessage(e2);
                                    if (err2.equals(SCard.SCARD_E_SHARING_VIOLATION)) {
                                        present = "[X]";
                                    }
                                }
                            } else {
                                secondline = "          " + err;
                            }
                        } finally {
                            if (c != null)
                                c.disconnect(false);
                        }

                        if (atr != null) {
                            secondline = "          " + HexUtils.bin2hex(atr).toUpperCase();
                            if (args.has(OPT_WEB)) {
                                String url = "http://smartcard-atr.appspot.com/parse?ATR=" + HexUtils.bin2hex(atr);
                                if (Desktop.isDesktopSupported()) {
                                    Desktop.getDesktop().browse(new URI(url + "&from=apdu4j"));
                                } else {
                                    thirdline = "          " + url;
                                }
                            }
                        }
                    }

                    System.out.println(present + vmd + t.getName());
                    if (secondline != null)
                        System.out.println(secondline);
                    if (thirdline != null)
                        System.out.println(thirdline);
                }
            }

            // Select terminals to work on
            if (args.has(OPT_READER)) {
                String reader = (String) args.valueOf(OPT_READER);
                CardTerminal t = terminals.getTerminal(reader);
                if (t == null) {
                    fail("Reader \"" + reader + "\" not found.");
                }
                do_readers = Arrays.asList(t);
            } else {
                do_readers = terminals.list(State.CARD_PRESENT);
                if (do_readers.size() == 0 && !args.has(CMD_LIST)) {
                    // But if there is a single reader, wait for a card insertion
                    List empty = terminals.list(State.CARD_ABSENT);
                    if (empty.size() == 1 && args.has(OPT_WAIT)) {
                        CardTerminal rdr = empty.get(0);
                        System.out.println("Please enter a card into " + rdr.getName());
                        if (!empty.get(0).waitForCardPresent(30000)) {
                            System.out.println("Timeout.");
                        } else {
                            do_readers = Arrays.asList(rdr);
                        }
                    } else {
                        fail("No reader with a card found!");
                    }
                }
            }
        } catch (CardException e) {
            // Address Windows with SunPCSC
            String em = TerminalManager.getExceptionMessage(e);
            if (em.equals(SCard.SCARD_E_NO_READERS_AVAILABLE)) {
                fail("No reader with a card found!");
            } else {
                System.out.println("Could not list readers: " + em);
            }
        }

        // If we have meaningful work with readers
        if (!hasWork(args))
            System.exit(0);

        // Do it
        for (CardTerminal t : do_readers) {
            if (do_readers.size() > 1 || args.has(OPT_VERBOSE)) {
                System.out.println("# " + t.getName());
            }
            try {
                work(t, args);
            } catch (CardException e) {
                if (TerminalManager.getExceptionMessage(e).equals(SCard.SCARD_E_SHARING_VIOLATION)) {
                    continue;
                }
                throw e;
            }
        }
    }

    private static boolean hasWork(OptionSet args) {
        if (args.has(CMD_APDU))
            return true;
        if (args.has(OPT_CONNECT))
            return true;
        return false;
    }

    private static void work(CardTerminal reader, OptionSet args) throws CardException {
        if (!reader.isCardPresent()) {
            System.out.println("No card in " + reader.getName());
            return;
        }

        if (args.has(OPT_VERBOSE)) {
            FileOutputStream o = null;
            if (args.has(OPT_DUMP)) {
                try {
                    o = new FileOutputStream((File) args.valueOf(OPT_DUMP));
                } catch (FileNotFoundException e) {
                    System.err.println("Can not dump to " + args.valueOf(OPT_DUMP));
                }
            }
            reader = LoggingCardTerminal.getInstance(reader, o);
        }

        // This allows to override the protocol for RemoteTerminal as well.
        String protocol;
        boolean transact = true;
        if (args.has(OPT_T0)) {
            protocol = "T=0";
        } else if (args.has(OPT_T1)) {
            protocol = "T=1";
        } else {
            protocol = "*";
        }
        // JNA-proprietary
        if (args.has(OPT_EXCLUSIVE)) {
            protocol = "EXCLUSIVE;" + protocol;
        } else if (System.getProperty("os.name").toLowerCase().contains("windows") && args.has(OPT_CONNECT)) {
            // Windows 8+ have the "5 seconds of transaction" limit. Because we want reliability
            // and don't have access to arbitrary SCard* calls via javax.smartcardio, we rely on
            // JNA interface and its EXCLUSIVE access instead and do NOT use the SCardBeginTransaction
            // capability of the JNA interface.
            // https://msdn.microsoft.com/en-us/library/windows/desktop/aa379469%28v=vs.85%29.aspx
            transact = false;
            protocol = "EXCLUSIVE;" + protocol;
        }

        if (args.has(CMD_APDU) || args.has(OPT_SHELL)) {
            Card c = null;
            try {
                c = reader.connect(protocol);

                if (args.has(CMD_APDU)) {
                    for (Object s : args.valuesOf(CMD_APDU)) {
                        CommandAPDU a = new CommandAPDU(HexUtils.stringToBin((String) s));
                        ResponseAPDU r = c.getBasicChannel().transmit(a);
                        if (args.has(OPT_ERROR) && r.getSW() != 0x9000) {
                            System.out.println("Card returned " + String.format("%04X", r.getSW()) + ", exiting!");
                            return;
                        }
                    }
                } else {
                    Shell s = new Shell(c);
                    s.run();
                    return;
                }
            } catch (CardException e) {
                if (TerminalManager.getExceptionMessage(e) != null) {
                    System.out.println("PC/SC failure: " + TerminalManager.getExceptionMessage(e));
                    return;
                } else {
                    throw e;
                }
            } finally {
                if (c != null) {
                    c.disconnect(true);
                }
            }
        } else if (args.has(OPT_CONNECT)) {
            String remote = (String) args.valueOf(OPT_CONNECT);
            JSONMessagePipe transport = null;
            KeyManagerFactory kmf = null;
            X509Certificate pinnedcert = null;


            try {
                // Connection parameters
                if (args.has(OPT_P12)) {
                    String[] pathpass = args.valueOf(OPT_P12).toString().split(":");
                    if (pathpass.length != 2) {
                        throw new IllegalArgumentException("Must be path:password!");
                    }
                    kmf = SocketTransport.get_key_manager_factory(pathpass[0], pathpass[1]);
                }
                if (args.has(OPT_PINNED)) {
                    pinnedcert = certFromPEM(((File) args.valueOf(OPT_PINNED)).getPath());
                }

                // Select transport
                if (remote.startsWith("http://") || remote.startsWith("https://")) {
                    transport = HTTPTransport.open(new URL(remote), pinnedcert, kmf);
                } else {
                    transport = SocketTransport.connect(string2socket(remote), null);
                }

                // Connect the transport and the terminal
                CmdlineRemoteTerminal c = new CmdlineRemoteTerminal(transport, reader);
                c.forceProtocol(protocol);
                c.transact(transact);
                // Run
                c.run();
            } catch (IOException e) {
                System.err.println("Communication error: " + e.getMessage());
            } finally {
                if (transport != null)
                    transport.close();
            }
        }
    }

    private static void help_and_exit(OptionParser parser, PrintStream o) throws IOException {
        System.err.println("# apdu4j command line utility\n");
        parser.printHelpOn(o);
        System.exit(1);
    }

    private static X509Certificate certFromPEM(String path) throws IOException {
        try {
            // TODO: use PEMParser
            String pem = new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.US_ASCII);
            String[] lines = pem.split("\n");
            String[] b64 = Arrays.copyOfRange(lines, 1, lines.length - 1);
            byte[] bytes = Base64.getDecoder().decode(String.join("", b64));
            CertificateFactory cf;
            cf = CertificateFactory.getInstance("X.509");
            X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(bytes));
            return cert;
        } catch (CertificateException e) {
            throw new IOException(e);
        }
    }

    public static String getVersion() {
        String version = "unknown-development";
        try (InputStream versionfile = SCTool.class.getResourceAsStream("pro_version.txt")) {
            if (versionfile != null) {
                try (BufferedReader vinfo = new BufferedReader(new InputStreamReader(versionfile, StandardCharsets.UTF_8))) {
                    version = vinfo.readLine();
                }
            }
        } catch (IOException e) {
            version = "unknown-error";
        }
        return version;
    }

    private static InetSocketAddress string2socket(String s) {
        String[] hostport = s.split(":");
        if (hostport.length != 2) {
            throw new IllegalArgumentException("Can connect to host:port pairs!");
        }
        return new InetSocketAddress(hostport[0], Integer.parseInt(hostport[1]));
    }

    private static void fail(String message) {
        System.err.println(message);
        System.exit(1);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy