为了配合接触式磁卡嗅探器的使用,以及方便复制修改卡片的内容
我找到了ACS公司的ACR39U的读卡器以及他的开发包,本来看开发包里面的源码和开发手册很全,心里一激动以为拿到就能用
但是真正拿到手里面的时候,发现里面的功能和代码错误和缺失了很大一部分,没办法,只能自己拿着源码再摸索着修改
如果你对于此读卡器的开发有些感兴趣的话,可以看下下面的内容
如果你只是想找个能用的软件的话,可以直接到本片文章的最下方
注意:本篇文章使用的语言为JAVA
写卡方法修改
16进制String转byte
磁卡里面的数据都是以byte存储,所以软件上免不了一个过程,就是String-byte-byte[]-String。
首先要解决的就是写数据与读数据时,他总是把一字节的16进制数据分成两个字节的字符,再写入字符的ascii值。
在读写方法里,我们看到一个charat↓
for (counter = 0; counter < temporaryData.length(); counter ++) //line 999
data[counter] = (byte)temporaryData.charAt(counter); //line 1000也就是说,这串代码会把所有读到的数据自动转换为char值,无论是读写都是。
于是,把这串代码码掉之后,我们重新换上了helper.java中的getbytes方法↓
public static byte[] getBytes(String stringBytes, String delimeter)
{
String[] arrayString = stringBytes.split(delimeter);
byte[] bytesResult = new byte[arrayString.length];
int counter;
for (counter = 0; counter < arrayString.length; counter++)
bytesResult[counter] = (byte)Integer.parseInt(arrayString[counter],16); //line 63
return bytesResult;
}
public static byte[] getBytes(String stringBytes)
{
String formattedString = "";
int temporaryCounter = 0;
int counter;
if(stringBytes.trim() == "")
return null;
for(counter = 0; counter < stringBytes.length(); counter++)
{
if(stringBytes.charAt(counter) == ' ')
continue;
if(temporaryCounter > 0 && temporaryCounter % 2 == 0)
formattedString += " ";
formattedString += stringBytes.charAt(counter);
temporaryCounter++;
}
return getBytes(formattedString, " ");
}在原始sdk里,为了将文本框里获取到的String转换为Byte,他使用了byte.parseByte的方法,但是这个方法会导致一个问题,就是数值越界。
也就是说当你转换9D这种数字的时候,他会直接报错。因为byte的取值范围为-128~127,而9D由16进制转换为10进制的结果是157。
于是就需要将数据的转换方法进行调整,改为了Integer.parseInt()的方法,但是与此同时,又出现一个问题。发现转换出来的数据.........变为负数了。
例如9D转换出来的数据,又变成了-99。
仔细检查了一下问题,发现是编码的问题,GBK采用双字节8位表示,总体编码范围为 8140 -- FEFE,首字节在 81 -- FE 之间,尾字节在 40 -- FE 之间。
ASCII是7位编码,只使用前7位,第8位补0,所以转换成整数始终为正数
而GBK是8位编码,也就是说一个字节中的第8位可以为1,如1010 1101,而将其转换成byte类型时,byte值为10101101,以补码存储,第8位被当成符号位,当然是负数了,值为:-83。
但是测试了一下,发现好像不影响读写数据,整体都正常,于是就没再修改了。
读卡方法修改
16进制byte[]转String
在读卡器读到数据之后,需要在文本框中显示出读卡结果。但是原始sdk里出现的数据却是一串乱码。
仔细看了下发现,原来是sdk里的byteArrayToString方法把所有获取到的数据都做了一个char的强制转换。
但是ascii码表里能显示的数据有限,于是就造成了像92 23 10这类数据读到之后就会编程乱码的情况。
public static String byteArrayToString (byte[] data)
{
String convertedString = "";
int counter;
for (counter = 0; counter < data.length; counter++)
{
convertedString += (char)data[counter];
}
return convertedString;
}既然是这样,那我们直接把char的强制转换删了不就行了吗?当我按下退格键之后,才发现......并不行。
之前还能显示乱码的数据,现在全变成上面提到的负值了,整个文本框里密密麻麻的负数,而且还没有空格,让人整个人脑子一愣。
还是和之前说到的编码有关,直接将byte的数值转换为string看来是不可能了。
于是重新找了下java1.8的开发手册,看看string里有没有相关的方法能用来转换格式,结果发现确实有→String.format()
public static String byteArrayToString (byte[] data)
{
String str = "";
for(int i = 0;i < data.length;i ++) {
if(i < data.length - 1) {
str += String.format("%02X", data[i]);
str += " ";
}else {
str += String.format("%02X", data[i]);
}
}
return str;
}前面代表了转换数据的格式,%02X是将转换的16进制数据自动变为两位,例如2就会变成02。后面就是要转换的数据,一般为byte格式。
将转换方法变成这样后,文本框里显示的数据就变得更清晰了,而且保证了数据的完整性,不会出现负数或者乱码的情况。
文本框修改
复制和粘贴
在文本框输入数据时,发现原始sdk里的文本框,既不支持复制,也不支持粘贴,只能一个一个的输入数据
于是翻了一下,发现在事件监听器中有一串代码,将v键直接设置为无反应。
public void keyPressed(KeyEvent keyEvent)
{
//Restrict paste actions
if (keyEvent.getKeyCode() == KeyEvent.VK_V )
keyEvent.setKeyCode(KeyEvent.VK_UNDO );
}不太理解为什么写这段源码的人要在本来就没有设置复制粘贴的事件监听器的情况下,还要限制文本框的复制和粘贴......
当你看到上帝在给你关上一扇门的时候,你根本想象不到,他连窗户都没开
private static String getclipboard() {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
Transferable trans = clipboard.getContents(null);
String text;
try {
text = (String) trans.getTransferData(DataFlavor.stringFlavor);
return text;
} catch (Exception e) {
text = null;
}
return text;
}
private static void setclipboard(String text) {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
Transferable trans = new StringSelection(text);
clipboard.setContents(trans, null);
}
public void keyPressed(KeyEvent keyEvent) {
if (_TextAreaDataMemoryCard.isFocusOwner()) {
if (keyEvent.isControlDown() && keyEvent.getKeyCode() == KeyEvent.VK_V) {
String text = getclipboard();
_TextAreaDataMemoryCard.append(text);
}
if (keyEvent.isControlDown() && keyEvent.getKeyCode() == KeyEvent.VK_C) {
String text = _TextAreaDataMemoryCard.getText();
setclipboard(text);
}
}
if (_TextAreaDataProtectionBits.isFocusOwner()) {
if (keyEvent.isControlDown() && keyEvent.getKeyCode() == KeyEvent.VK_V) {
String text = getclipboard();
_TextAreaDataProtectionBits.append(text);
}
if (keyEvent.isControlDown() && keyEvent.getKeyCode() == KeyEvent.VK_C) {
String text = _TextAreaDataProtectionBits.getText();
setclipboard(text);
}
}
}isfocusowner()代表:当文本框被选中时,将要进行的操作。
iscontroldown()和getkeycode代表:ctrl被按下时另一个按键也被同时按下。
getclipboard()和setclipboard():在复制和粘贴时对于系统剪贴板的动作
然后这里的粘贴,使用了append这个方法而不是settext,这样当你按下ctrl+v的时候,文本框做的动作就是增加文本,而不是把文本框里的内容给重置。
文本框输入长度限制
在原始的软件里还存在一个很关键的问题,输入长度被限制!
sdk里默认对卡片的读写,都是基于char来进行的,也就是说,无论你输入什么,他都会自动的转换为char,然后取他的ascii码来写入
除此之外还非常贴心地设置了一个长度限制,以防止用户输入数据的时候超过了允许操作的范围
于是以上的操作就导致了,在写入过程中,不仅你写不了自己原本想要写入的数据,而且数据写到一半,他就会限制输入
注:关于数据的转码,string和byte[]之间的互相转换,在前面几个方法里已经提到过了
在读卡器软件里的输入限制和普通文本框的输入限制有几个不一样的地方
普通的文本框,如果想要限制数据的话,直接判断文本框内输入的长度,然后将超出的键值设置为空就行。就如下图所示↓
public void keyTyped(KeyEvent keyEvent)
{
Character x = (Character)keyEvent.getKeyChar();
char empty = '\r';
byte temporaryMaximumLength;
int maximumLength = 2;
//Check valid characters
if(_TextFieldCode1.isFocusOwner() || _TextFieldCode2.isFocusOwner() || _TextFieldCode3.isFocusOwner() ||
_TextFieldAddress1.isFocusOwner() || _TextFieldAddress2.isFocusOwner() || _TextFieldSecurityAddress1.isFocusOwner() ||
_TextFieldSecurityAddress2.isFocusOwner() || _TextFieldLength.isFocusOwner() || _TextFieldSecurityLength.isFocusOwner())
{
if (VALID_CHARACTER_HEX.indexOf(x) == -1)
keyEvent.setKeyChar(empty);
}
//Limit character length
if(_TextFieldCode1.isFocusOwner() || _TextFieldCode2.isFocusOwner() || _TextFieldCode3.isFocusOwner() ||
_TextFieldAddress1.isFocusOwner() || _TextFieldAddress2.isFocusOwner() || _TextFieldSecurityAddress1.isFocusOwner() ||
_TextFieldSecurityAddress2.isFocusOwner() || _TextFieldLength.isFocusOwner() || _TextFieldSecurityLength.isFocusOwner())
{
if (((JTextField)keyEvent.getSource()).getText().length() >= maximumLength)
{
keyEvent.setKeyChar(empty);
return;
}
}
else if(_TextAreaDataMemoryCard.isFocusOwner())
{
if (_TextFieldLength.getText().trim().equals(""))
maximumLength = 0;
else
{
temporaryMaximumLength = (byte)((Integer)Integer.parseInt(_TextFieldLength.getText(), 16)).byteValue();
maximumLength = temporaryMaximumLength & 0xFF;
}
if (((JTextArea)keyEvent.getSource()).getText().length() >= maximumLength)
{
keyEvent.setKeyChar(empty);
return;
}
}
else if (_TextAreaDataProtectionBits.isFocusOwner())
{
if (_TextFieldSecurityLength.getText().trim().equals(""))
maximumLength = 0;
else
{
temporaryMaximumLength = (byte)((Integer)Integer.parseInt(_TextFieldSecurityLength.getText(), 16)).byteValue();
maximumLength = temporaryMaximumLength & 0xFF;
}
if (((JTextArea)keyEvent.getSource()).getText().length() >= maximumLength)
{
keyEvent.setKeyChar(empty);
return;
}
}
}但是读卡器软件里的文本框输入限制,除了对长度进行判断之外,还需要对数据之间的空格进行判断。
因为读卡器读到的字节与字节之间会自动带一个空格来表示隔断,所以如果想对已经读出的数据进行修改的话,那么防呆设置里就需要对空格进行替换,并且对数据的长度进行重新计算。
于是就如下图所示↓
public void keyTyped(KeyEvent keyEvent) {
Character x = (Character) keyEvent.getKeyChar();
int temporaryMaximumLength;
int maximumLength = 2;
//Check valid characters
if (_TextFieldCode1.isFocusOwner() || _TextFieldCode2.isFocusOwner() || _TextFieldCode3.isFocusOwner() ||
_TextFieldAddress1.isFocusOwner() || _TextFieldAddress2.isFocusOwner() || _TextFieldSecurityAddress1.isFocusOwner() ||
_TextFieldSecurityAddress2.isFocusOwner() || _TextFieldLength.isFocusOwner() || _TextFieldSecurityLength.isFocusOwner()) {
if (VALID_CHARACTER_HEX.indexOf(x) == -1)
keyEvent.consume();
}
//Limit character length
if (_TextFieldCode1.isFocusOwner() || _TextFieldCode2.isFocusOwner() || _TextFieldCode3.isFocusOwner() ||
_TextFieldAddress1.isFocusOwner() || _TextFieldAddress2.isFocusOwner() || _TextFieldSecurityAddress1.isFocusOwner() ||
_TextFieldSecurityAddress2.isFocusOwner() || _TextFieldLength.isFocusOwner() || _TextFieldSecurityLength.isFocusOwner()) {
if (((JTextField) keyEvent.getSource()).getText().length() >= maximumLength) {
keyEvent.consume();
return;
}
}
if(_TextAreaDataMemoryCard.isFocusOwner())
{
if (_TextFieldLength.getText().trim().equals(""))
temporaryMaximumLength = 0;
else
{
temporaryMaximumLength = (Integer)Integer.parseInt(_TextFieldLength.getText(), 16);
temporaryMaximumLength *= 2;
}
if (Helper.countchar( ((JTextArea)keyEvent.getSource()).getText()) >= temporaryMaximumLength)
{
keyEvent.consume();
return;
}
}
if (_TextAreaDataProtectionBits.isFocusOwner())
{
if (_TextFieldSecurityLength.getText().trim().equals(""))
temporaryMaximumLength = 0;
else
{
temporaryMaximumLength = (Integer)Integer.parseInt(_TextFieldSecurityLength.getText(), 16);
temporaryMaximumLength *= 2;
}
if (Helper.countchar (((JTextArea)keyEvent.getSource()).getText()) >= temporaryMaximumLength)
{
keyEvent.consume();
return;
}
}关于Helper.countchar里的代码:↓
public static int countchar(String text){
String temptext = text.replaceAll(" ","");
int count = temptext.length();
return count;
}相当于在原始的基础上面,直接把限制的数量乘以二,然后对于文本框里的内容去除空格后重新计算长度,并且省略了无意义的转换byte这一步。
以上四点是将读卡器软件中所有不合理的地方都做了修改,使软件能够进行正常的读写
对于4442卡、4428卡的一键读写功能
当时修改这个软件是为了给迅达的电梯工人制卡使用,但是对于磁卡的存储区和长度这些概念,他们并不太方便理解
所以为了他们操作方便,我在软件里又增加了关于4442卡和4428卡的一键读写功能
即:将卡片的数据一键读取,并保存为16进制文件。将16进制文件内的数据读出,并一键写入卡片
一键读取
这一步里我直接将sle类中的readmemorycard方法重复调用,然后将得到的数据分别存储进两个数组中
然后将两组数据通过addbytes方法相加在一起,最后将结果写入文件中
//根据选择的卡片类型读取卡片数据
if (_ComboBoxCardTypeList.getSelectedIndex() == SLE4432_SLE4442_SLE5542) {
data1 = _sle.readMemoryCard(add4442_1, (byte) 0xff);
data5 = _sle.readMemoryCard(add4442_2, (byte) 0x01);
data4442 = addbytes(data1, data5);
}
//将两组数据相加整合在一起
public byte[] addbytes(byte[] data1, byte[] data2) {
byte[] result = new byte[data1.length + data2.length];
System.arraycopy(data1, 0, result, 0, data1.length);
System.arraycopy(data2, 0, result, data1.length, data2.length);
return result;
}
//将结果输出为一个16进制文件
private void ExportFile() {
boolean result;
byte[] data;
try {
chooser.showSaveDialog(fileoutframe);
File f = chooser.getSelectedFile();
String fname = f.getName();
File file = new File(chooser.getCurrentDirectory() + "/" + fname);
readallbytes();
if (_ComboBoxCardTypeList.getSelectedIndex() == SLE4418_SLE4428_SLE5528) {
data = data4428;
if (data != null) {
result = writefile(data, file);
if (result == true) {
addTitleToLog("导出文件成功");
} else {
addTitleToLog("导出文件失败");
}
} else {
addTitleToLog("获取到的数据为空,请检查卡片重新操作");
}
} else if (_ComboBoxCardTypeList.getSelectedIndex() == SLE4432_SLE4442_SLE5542) {
data = data4442;
if (data != null) {
result = writefile(data, file);
if (result == true) {
addTitleToLog("导出文件成功");
} else {
addTitleToLog("导出文件失败");
}
} else {
addTitleToLog("获取到的数据为空,请检查卡片重新操作");
}
}
} catch (Exception e) {
showErrorMessage("导出文件未成功,请检查操作日志");
}
}一键写入
这一步里先把将要写入的文件进行读取,然后判断他的数据长度是否符合所选择的卡片类型
判断符合要求后,将文件分割为几个255长度的数组,再进行写入卡片的操作
//选择文件,并读取文件数据
private void ImportFile() {
byte[] data;
chooser.showOpenDialog(fileinframe);
File file = chooser.getSelectedFile();
data = readfile(file);
boolean r = writeallbytes(data);
if (r) {
addTitleToLog("读文件及写卡成功");
} else {
showErrorMessage("读文件及写卡失败,请检查文件或卡片");
}
}
//读取文件数据
public byte[] readfile(File file) {
try {
FileInputStream fis = new FileInputStream(file);
byte[] a = new byte[(int) file.length()];
fis.read(a);
fis.close();
return a;
} catch (IOException e) {
return null;
}
}
//判断读取到的数据长度是否符合所选择的卡片类型,符合后写入,并向importfile返回一个boolean值
public boolean writeallbytes(byte[] data) {
apdu = new Apdu();
try {
if (_ComboBoxCardTypeList.getSelectedIndex() == SLE4418_SLE4428_SLE5528) {
if (data.length >= 1021 && data.length <= 1024) {
addTitleToLog("写第一段数据中......");
data1 = subbytes(data, 0, 255);
_sle.writeMemoryCard(add4428_1,data1,(byte)0xff);
addTitleToLog("写第二段数据中......");
data2 = subbytes(data, 255, 255);
_sle.writeMemoryCard(add4428_2,data2,(byte)0xff);
addTitleToLog("写第三段数据中......");
data3 = subbytes(data, 510, 255);
_sle.writeMemoryCard(add4428_3,data3,(byte)0xff);
addTitleToLog("写第四段数据中......");
data4 = subbytes(data, 765, 255);
_sle.writeMemoryCard(add4428_4,data4,(byte)0xff);
addTitleToLog("写第五段数据中......");
data5 = subbytes(data, 1020, 1);
_sle.writeMemoryCard(add4428_5,data5,(byte)0x01);
return true;
} else {
showErrorMessage("文件内数据长度非1024或1021字节,请检查文件");
return false;
}
} else if (_ComboBoxCardTypeList.getSelectedIndex() == SLE4432_SLE4442_SLE5542) {
if (data.length == 256) {
data1 = subbytes(data, 0, 255);
data5 = subbytes(data, 255, 1);
addTitleToLog("写第一段数据中......");
_sle.writeMemoryCard(add4442_1,data1,(byte)0xff);
addTitleToLog("写第二段数据中......");
_sle.writeMemoryCard(add4442_2,data5,(byte)0x01);
return true;
} else {
showErrorMessage("文件内数据长度非256字节,请检查文件");
return false;
}
}
} catch (Exception ex) {
return false;
}
return false;
}注意:
因为4428卡的最后三位是卡片的错误计数以及密码,所以在一键写入时,无论文件内的数据长度是1021还是1024,我们默认只写1021位
因为对于卡片的读写都需要使用apdu指令,而apdu指令每次操作的最大长度为0xFF,所以在读取和写入时,我们使用了addbytes和sudbytes方法将数据进行了拆分或组合
我这里使用的开发环境为jdk1.8
如果需要将jar打包为exe文件使用,可以使用exe4j,打包环境推荐使用jre1.8
分割线--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------jar包、源码、打包好的程序、读卡器开发手册→https://www.aliyundrive.com/s/32WdYXJQJEt 提取码: 1q1e
代码文件无法在阿里云盘上面被分享出来,所以我放在了github:https://github.com/mana-feng/ACR39U-card-reader
可以自己根据需要使用