წინამდებარე სტატია გიჩვენებთ, როგორ შეიძლება Java პროგრამირების ენის გამოყენებით ID ბარათის MIFARE Classic 1k–ს ემულატორთან მუშაობა. კერძოდ - მისი ყველა სექტორის ყველა ბლოკის წაკითხვა.

კოდი განკუთვნილია სამაგალითოდ და არ წარმოადგენს რაიმე რეალური ამოცანის გადაწყვეტას. თუმცა ის წარმოდგენას შეგიქმნით, რა ტიპის ბრძანებები უნდა გაუგზავნოთ თქვენს წამკითხველს რათა მან შესძლოს ID ბარათიდან ან სხვა MIFARE Classic 1k თავსებადი ბარათიდან ინფორმაციის ამოკითხვა.

პროგრამა დაწერილი და შემოწმებულია HID OMNIKEY 5321 USB მოდელის წამკითხველებზე. სხვა მოდელის წამკითხველებზე მის გამოყენებას შეიძლება დასჭირდეს დამატებითი ადაპტაცია. პირველ რიგში სცადეთ გამოიყენოთ EMULATED = false;

პროგრამის დაწერისას გამოყენებულია შემდეგი ინსტრუქცია: www.hidglobal.com/documents/ok_contactless_developer_guide_an_en.pdf და მისი წაკითხვა რეკომენდებულია ყველასთვის ვისაც ამ რიდერის გამოყენება უნდა.

ასევე რეკომენდებულია გადახედოთ სტატიას MIFARE Classic 1k ემულატორი ID ბარათზე

პროგრამა იყენებს PC/SC ინტერფეისს. აქედან გამომდინარე, მისი პორტირება შესაძლებელია ნებისმიერ სხვა პროგრამირების ენაზე, რომლის საშუალებითაც PC/SC ინტერფეისთან მუშაობაა შესაძლებელი.

package ge.eid.card.mifare;

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.List;
import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.TerminalFactory;

/**
 * ეს პროგრამული კოდი კითხულობს ყველა სექტორის ყველა ბლოკს MIFARE Classic 1k
 * ემულატორიდან. პროგრამა გატესტილია HID OMNIKEY 5321 ოჯახის წამკითხველებზე
 * და მუშაობს მხოლოდ უკონტაქტო ინტერფეისით (თუმცა ზოგიერთ მოდელს შეიძლება 2
 * ინტერფეისი ჰქონდეს. კონტაქტურიც და უკონტაქტოც)
 * 
 * პროგრამა იყენებს PC/SC ინტერფეისს, რომელიც სტანდარტულია როგორც Windows-ში,
 * ისე სხვა ოპერაციულ სისტემებში. ამ უკანასკნელებში pcsc lite-ს გამოყენებით
 * 
 * @author mikheil
 */
public class Main {

    // IOCTL ბრძანება რომლითაც შესაძლებელია ბარათის წამკითხველში MIFARE
    // ემულაციის გამორთვა - ემულაციას აკეთებს თავად ბარათი
    public static final int CM_IOCTL_SET_RFID_CONTROL_FLAGS = scardCtlCode(3213);

    private static final boolean EMULATED = true;
    private static final boolean FACTORY_KEYS = false;
    private static final boolean ZERO_KEYS = false;

    /**
     * PC/SC Control Code (საკონტროლო კოდი) სხვადასხვანაირად ითვლება
     * 
     * @param code
     * @return
     */
    public static int scardCtlCode(int code) {
        String osName = System.getProperty("os.name").toLowerCase();
        if (osName.indexOf("windows") > -1) {
            return 0x31 << 16 | code << 2;
        } else {
            return 0x42000000 + code;
        }
    }

    /**
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        // ავირჩიოთ ტერმინალი
        CardTerminal terminal = selectCardTerminal();

        // დაველოდოთ ბარათს
        System.out.println("Waiting for a card..");
        terminal.waitForCardPresent(0);

        // დავუკავშირდეთ ბარათს. იხ დოკუმენტაცია თავად მეთოდზე
        Card card = getCardConnection(terminal, EMULATED);

        try {

            // ავიღოთ კავშირის არხი
            CardChannel channel = card.getBasicChannel();

            // შეგვიძლია წავიკითხოთ ბარათის UID
            byte[] readUIDCommand = new byte[] { (byte) 0xFF, (byte) 0xCA, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00 };
            System.out.println("UID: " + send(readUIDCommand, channel, true));
            // წარმატებული წაკითხვის შემთხვევაში UID-ს შემდეგ ეწერება 9000

            // ჩავტვირთოთ A გასაღები წამკითხველის მეხსიერებაში
            // მაგალითად, 0x07 ზონაში
            byte[] loadKeyCommand = createKeyLoadingCommand((byte) 0x07);
            System.out.println("LOAD KEY: " + send(loadKeyCommand, channel, true));
            // წარმატების შემთხვევაში გამოვა 9000

            byte[] readCommand;
            // გადავუაროთ ყველა ბლოკს (16 სექტორი, 4 ბლოკი თითოეულში)
            for (int blockId = 0; blockId < 4 * 16; blockId++) {
                // თითოეული სექტორისათვის საკმარისია ერთ ბლოკზე იდენტიფიკაცია
                // ამიტომაც ჩავატაროთ პროცესი დასაწყისში და ყოველ მეოთხე ბლოკზე
                if (blockId % 4 == 0) {
                    System.out.println();
                    System.out.println("Authenticating Sector " + (blockId / 4)
                            + " block 3");
                    byte authBlockId = (byte) (blockId + 3);
                    byte keyAddress = (byte) 0x07;
                    byte[] authCommand = createBlockAuthenticationCommand(
                            authBlockId, keyAddress);
                    System.out.println("AUTHENTICATE: "
                            + send(authCommand, channel, true));
                    // წარმატებული აუთენტიფიკაციის შემთხვევაში ეწერება 9000
                }

                // თუ იდენტიფიკაცია გავლილია, შეგვიძლია წავიკითხოთ ბლოკი
                readCommand = new byte[] { (byte) 0xFF, (byte) 0xB0, (byte) 0x00,
                        (byte) (blockId), (byte) 0x10 };
                System.out.println("READ: " + send(readCommand, channel, false));
                // წარმატებული წაკითხვის შემთხვევაში მონაცემების შემდეგ ეწერება
                // 9000
            }
        } finally {
            card.disconnect(true);
        }
    }

    /**
     * ამ მეთოდით ხდება იდენტიფიკაციის ბრძანების აწყობა
     * 
     * @param blockId
     *            ბლოკი, რომლის მიმართაც უნდა გავიაროთ აუთენტიფიკაცია
     * @param keyAddress
     *            მისამართი, სადაც ჩატვირთული იყო გასაღები
     */
    private static byte[] createBlockAuthenticationCommand(byte blockId,
            byte keyAddress) {
        byte keyTypeId = (byte) 0x60; // A გასაღები
        byte[] authData = new byte[] { (byte) 0x01, 0, blockId, keyTypeId,
                keyAddress };
        // ავაწყოთ აუთენტიფიკაციის ბრძანება
        CommandAPDU c1 = new CommandAPDU(0xFF, 0x86, 0x00, 0x00, authData);
        byte[] authDateBytes = c1.getBytes();
        return authDateBytes;
    }

    /**
     * ამ მეთოდით ხდება წამკითხველის მეხსიერებაში გასაღების ჩატვირთვის ბრძანების
     * აწყობა
     * 
     * @param keyAddress
     *            მისამართი, სადაც ჩატვირთული იყო გასაღები
     */
    private static byte[] createKeyLoadingCommand(byte loadKeyTo) {
        byte[] loadKeyCommand;
        if (FACTORY_KEYS) {
            // მწარმოებლის გასაღებები. მოჰყვება ცარიელ ბარათებს
            loadKeyCommand = new byte[] { (byte) 0xFF, (byte) 0x82, (byte) 0x20,
                    loadKeyTo, (byte) 0x06, (byte) 0xFF, (byte) 0xFF,
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF };
        } else {
            if (ZERO_KEYS) {
                // ნულოვანი გასაღებები
                loadKeyCommand = new byte[] { (byte) 0xFF, (byte) 0x82, (byte) 0x20,
                        loadKeyTo, (byte) 0x06, (byte) 0x00, (byte) 0x00,
                        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
            } else {
                // MAD სტანდარტული A გასაღებები, რომელიც ადევს MAD სექტორს.
                // ID ბარათზე ყველა ცარიელ საქტორს სექტორს იგივე გასაღები უყენია
                // გასაღების მნიშვნელობაა A0A1A2A3A4A5
                loadKeyCommand = new byte[] { (byte) 0xFF, (byte) 0x82, (byte) 0x20,
                        loadKeyTo, (byte) 0x06, (byte) 0xA0, (byte) 0xA1,
                        (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5 };
            }
        }
        return loadKeyCommand;
    }

    /**
     * ამ მეთოდით ხდება წამკითხველთან დაკავშირება. OMNIKEY 5321 USB ავტომატურად
     * ააქტიურებს MIFARE ემულატორს თავად წამკითხველში, ამიტომ ის არ მუშაობს ID
     * ბარათთან. იმისათვის, რომ წამკითხველმა იმუშაოს ID ბარათთან, აუცილებელია
     * ემულაციის რეჟიმის გამორთვა. MIFARE ემულაციას თვითონ ბარათი აკეთებს.
     * 
     * ემულაციის გამორთვა ასევე შეიძლება სისტემური რეესტრიდან იხილეთ დოკუმენტი
     * www.hidglobal.com/documents/ok_contactless_developer_guide_an_en.pdf
     * 
     * @param terminal
     *            მისამართი, სადაც ჩატვირთული იყო გასაღები
     * @param onCardEmulation
     *            თუ true-ა, მეთოდი გამორთავს წამკითხველზე MIFARE ემულაციას
     */
    private static Card getCardConnection(CardTerminal terminal,
            boolean onCardEmulation) throws CardException {
        Card card = terminal.connect("T=0");
        if (onCardEmulation) {
            // გავუშვათ ემულაციის ბრძანება
            byte[] ioctl = new byte[] { 4, 0, 0, 0, 4, 0, 0, 0 };
            byte[] resp = card.transmitControlCommand(
                    CM_IOCTL_SET_RFID_CONTROL_FLAGS, ioctl);
            System.out.println("IOCTL Sent");
            System.out.println(new BigInteger(1, resp).toString(16));
            // ბრძანების გაშვების შემდეგ წამკითხველი გაითიშება და თავიდან
            // ჩაირთვება. ამიტომ საჭიროა მასთან თავიდან დაკავშირება
            terminal.waitForCardPresent(0);
            card = terminal.connect("T=0");
        }
        return card;
    }

    /**
     * ბარათის წამკითხველის არჩევა. მეთოდი იყენებს მარტივ გზას მკითხველს შეუძლია
     * გაცილებით უფრო დახვეწილი გზა გამოიყენოს. მაგალითად შეეკითხოს მომხმარებელს
     * ან აიღოს კონფიგურაციის ფაილიდან
     */
    private static CardTerminal selectCardTerminal() throws CardException {
        CardTerminal terminal = null;
        // show the list of available terminals
        TerminalFactory factory = TerminalFactory.getDefault();
        List<CardTerminal> terminals = factory.terminals().list();

        for (int i = 0; i < terminals.size(); i++) {
            String terminalFull = terminals.get(i).toString();

            System.out.println("Terminal: " + terminalFull);

            if (terminalFull.contains("CL")) {
                terminal = terminals.get(i);
                System.out.println("SELECTED Terminal: " + terminalFull);
            }
        }
        return terminal;
    }

    /**
     * ბრძანების გაგზავნა წამკითხველთან და პასუხის მიღება
     * 
     * @param cmd
     *            ბრძანება ბაიტების მასივში
     * @param channel
     *            კავშირის არხი ბარათთან
     * @param echo
     *            გამოვიდეს თუ არა ბრძანება ეკრანზე?
     */
    public static String send(byte[] cmd, CardChannel channel, boolean echo) {
        String res = "";
        for (int i = 0; i < cmd.length; i++) {
            res += String.format("%02X", cmd[i]);
            // The result is formatted as a hexadecimal integer
        }
        if (echo) {
            System.out.println("Sending " + res);
        }
        byte[] baResp = new byte[258];
        ByteBuffer bufCmd = ByteBuffer.wrap(cmd);
        ByteBuffer bufResp = ByteBuffer.wrap(baResp);
        // output = The length of the received response APDU
        int output = 0;
        try {
            output = channel.transmit(bufCmd, bufResp);
        } catch (CardException ex) {
            ex.printStackTrace();
        }
        res = "";
        for (int i = 0; i < output; i++) {
            res += String.format("%02X", baResp[i]);
            // The result is formatted as a hexadecimal integer
        }
        return res;
    }
}